[Unity] UniRxのObjectPoolを使ってParticleSystemを管理する

## はじめに
こんにちは。Life is Tech! メンターのみったにです。
普段はUnityコース、Minecraftコース、Androidコースなどを担当しています。

この記事は[Life is Tech ! Kansai Advent Calendar 2022](https://qiita.com/advent-calendar/2022/life-is-tech-kansai)の16日目の記事です。
前回の記事は@Veggie-Adelie-Kenjinの [映像編集のすゝめ](https://qiita.com/Veggie-Adelie-Kenjin/items/e2fb28a7879ea2aae4b4) でした。

この記事では、UniRxのObjectPoolを使ってParticleSystemを管理する方法を紹介します。

### 検証環境
- Unity : 2021.3.4f1
- UniRx - Reactive Extensions for Unity : Version 7.1.0
- Windows10


## Object Pool とは
**ObjectPool**（オブジェクトプール）とは、オブジェクトを使いまわすための機構のことです。

Unityではオブジェクトを生成するときにInstantiate、削除するときにDestroyを使いますが、InstantiateとDestroyのどちらもそれなりに重い処理であるため、多用するとゲームの処理が重くなる恐れがあります。
特にゲームのエフェクトは大量に生成されることが多く、毎回Instantiateをしてエフェクトを再生し、再生が終了したらDestroyをしていてはフレームレートを著しく下げてしまいます。

そこで今回はこの問題を解決するためのデザインパターンの**ObjectPool**を用いて、生成したエフェクトを削除せずに非表示にして、必要になったら再度表示してエフェクトを再生する仕組みを実装していきます。

## 実装
### 1. UniRxの導入
UniRxでは、ObjectPoolを作成する機構が最初から用意されています。
今回はUniRxを用いて実装をするので、以下のURLから自分のUnityプロジェクトにUniRxを入れておいてください。

https://assetstore.unity.com/packages/tools/integration/unirx-reactive-extensions-for-unity-17276?locale=ja-JP

### 2. エフェクト本体にアタッチするスクリプトの作成
まずはパーティクル本体にアタッチするスクリプトを作成します。
```c#:ParticleObject.cs
using System;
using UnityEngine;
using UniRx;

public class ParticleObject : MonoBehaviour
{
    private ParticleSystem particle;

    private void Awake()
    {
        particle = GetComponent<ParticleSystem>();
    }

    public IObservable<Unit> PlayParticle(Vector3 position)
    {
        transform.position = position;
        particle.Play();

        // ParticleSystemのstartLifetimeに設定した秒数が経ったら終了通知
        return Observable.Timer(TimeSpan.FromSeconds(particle.main.startLifetimeMultiplier))
            .ForEachAsync(_ => particle.Stop());
    }
}
```
PlayParticleメソッドは引数にパーティクルを再生したい座標を入れます。
今回はPlayParticleメソッドのreturn文でParticleSystemのStartLifetimeに設定した秒数が経ったら終了通知をするようにしていますが、```FromSeconds()```の中身を変更することで任意の時間で終了通知をするように変更できます。

作成したParticleObject.csは、使用するParticleSytemにアタッチし、Prefabにしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/729021/d2ad6946-6f59-4d84-f73c-07ab2fea92f0.png)



### 3. ObjectPoolの作成
次に、**UniRx.Toolkit.ObjectPool**を用いて先ほど作成したParticleObjectのObjectPoolを作成します。
```C#:ParticlePool.cs
using UnityEngine;
using UniRx.Toolkit;

public class ParticlePool : ObjectPool<ParticleObject>
{
    private readonly ParticleObject _prefab;
    private readonly Transform _parentTransform;

    public ParticlePool(Transform transform, ParticleObject prefab)
    {
        _parentTransform = transform;
        _prefab = prefab;
    }

    protected override ParticleObject CreateInstance()
    {
        var e = Object.Instantiate(_prefab, _parentTransform, true);

        return e;
    }
}
```
ObjectPool自体はMonoBehaviourを継承していないため、コンストラクタが必要です。
また、ObjectPool自体はUnityコンポーネントとしては振舞うことができないので注意してください。

```CreateInstance()```はオブジェクトの追加生成時に実行されます。
このとき、ParticleSystemを大量に生成した際にHierarchyビューがParticleSystemで散らからないようにするため、ParticlePoolをインスタンス化したオブジェクトが親オブジェクトとして生成したParticleSystemを管理する仕組みにしています。

### 4. ObjectPoolを利用するスクリプトの作成
ObjectPoolをインスタンス化し、管理するスクリプトを作成します。
```C#:ParticleGenerator.cs
using UnityEngine;
using UniRx;
using UniRx.Triggers;


public class ParticleGenerator : MonoBehaviour
{
    public static ParticleGenerator Instance;
    
    [SerializeField, TooltipAttribute("パーティクルのPrefab")]
    private GameObject particlePrefab;
    private ParticlePool particlePool;
    
    void Awake ()
    {
        // シングルトンの実装
        if (Instance == null) {
            Instance = this;
            DontDestroyOnLoad (gameObject);
        }
        else {
            Destroy (gameObject);
        }
    }
    
    private void Start()
    {
        //ObjectPoolを生成
        particlePool = new ParticlePool(transform, particlePrefab.GetComponent<ParticleObject>());
        
        //破棄されたとき（Disposeされたとき）にObjectPoolを解放する
        this.OnDestroyAsObservable().Subscribe(_ => particlePool.Dispose());
    }
    
    public void GenerateParticle(Vector3 position)
    { 
        //ObjectPoolから1つ取得
        var effect = particlePool.Rent();
        
        //エフェクトを再生し、再生終了したらpoolに返却する
        effect.PlayParticle(position)
            .Subscribe(__ => { particlePool.Return(effect); });
    }
}
```
Awake()の中でParticleGeneratorクラスを**シングルトン**にしています。
シングルトンにすることでシーン内の他のオブジェクトからGetComponentやFindをしなくてもこのスクリプトを呼ぶことができます。
呼び出し方については[使用例](#使用例)を参考にしてください。

Start()の中でObjectPoolをインスタンス化し、ObjectPoolを生成します。
このとき、1つのParticleObjectに対して1つのObjectPoolを生成するので、複数種類のParticleSystemを管理したい場合は別途ObjectPoolを生成することで実現できます。
その際はGenerateParticle()の中身も複数種類に対応できるように改修してください。
<br>
<br>
作成したParticleGenerator.csは、空のゲームオブジェクトに貼り付けてください。
貼り付けたゲームオブジェクトの子オブジェクトとしてParticleSystemが生成されます。
また、Inspectorビューに表示されているParticlePrefabには、先ほど作成したParticleObjectがアタッチされているPrefabを入れてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/729021/39e76839-547d-5da3-4974-65c3548198c5.png)
これでObjectPoolの実装は完了です。



## 使用例
今回作成したParticleSystemのObjectPoolの使用例について紹介します。
### 5. 実際にParticleSystemを生成するスクリプトの作成
今回は、キーボードのPを押したときにエフェクトをランダムな位置に生成するスクリプトを作成します。
```C#:TestParticleUser.cs
using UnityEngine;
using Random = UnityEngine.Random;

public class TestParticleUser : MonoBehaviour
{
    [SerializeField] private Vector3 randomPos = new Vector3(-4.0f, 3.0f, 4.0f);

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            float rx = Random.Range(0.0f, 1.0f) * randomPos.x;
            float ry = Random.Range(0.0f, 1.0f) * randomPos.y;
            float rz = Random.Range(0.0f, 1.0f) * randomPos.z;

            ParticleGenerator.Instance.GenerateParticle(new Vector3(rx,ry,rz));
        }
    }
}
```
ParticleGeneratorはシングルトン化しているので、```ParticleGenerator.Instance.GenerateParticle(座標);```と書けば、たった1行でエフェクトの再生とObjectPoolでの管理をすることができます。

### 動作確認
![ObjectPool_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/729021/f5de6070-c0ce-ff19-1e29-5ba9e6f12067.gif)
ObjectPoolに貯めてある個数以上のオブジェクトが必要になった場合はその都度オブジェクトを生成してエフェクトを再生し、使用可能なオブジェクトがある場合は使いまわして再生している。


## さいごに
今回は負荷軽減の手法の一つのObjectPoolに関してUniRxのObjectPoolを使った事例を紹介してみました。
今回記事を執筆するにあたって、 [打田恭平『UniRx/UniTask完全理解　より高度なUnity C#プログラミング』（株式会社ドワンゴ、2020）](https://www.kadokawa.co.jp/product/302009000670/)の内容を参考にさせていただきました。
ObjectPoolの考え方は何もオブジェクトの生成削除だけに適用されるものではなく、オーディオ管理などにも用いられています。
これを機に読者の皆様にもObjectPoolについていろいろ取り組んでいただけたらと思います。

ここからは余談ですが、なんとか記事を完成に持っていけました。
なかなかQiitaなどの記事を書く機会がなく、今回が初めて本格的に執筆した記事になります。
今までたくさんUnityの開発はしてきましたが、このような形でアウトプットするのは初めてだったので、いい機会になりました。
これを機にこれからも記事を書いてアウトプットをしていくのでよろしくお願いします。


さて、明日の[Life is Tech ! Kansai Advent Calendar 2022](https://qiita.com/advent-calendar/2022/life-is-tech-kansai)は @mo-mon1217の記事になります。
Shaderの記事のようです。Shaderは画面の絵作りに欠かせない技術で、自分も勉強中なので楽しみです。

以上、Life is Tech! メンターのみったにでした。