UniRxで簡単にオブジェクトプールを作る UniRx.Toolkit

2020年5月7日木曜日

UniRx Unity

t f B! P L

unity1week お題「密」にてオブジェクトプールを使いたいと思って調べたところUniRxを使うと簡単に作れることがわかりました。

UniRxか、あのよくわからん奴、、UniRxの部分はほぼコピペで行けます。

なので本当に簡単です。

では作っていきましょう。
(ここではUnity2Dでバージョン2019.3.12f1で作っていきます。)

UniRxをインポート

AssetSrore UniRx
でダウンロードして、Unityプロジェクトへインポート

すると、projectフォルダは以下のように「Plugins」が追加され、その中に「UniRx」というものができます。

オブジェクトプールの基本構成

オブジェクトプールは
・プールされるオブジェクト(沢山つくられる物)
・プール
・マネージャー
の3つのスクリプトで構成されます。

なので、まずは3つ、C#スクリプトを作りましょう。
それぞれ、「PooledObjects.cs」、「Pool.cs」、「Manager.cs」としました

最もシンプルなスクリプトを作ってみる

PooledObjects.cs

これは「プールされるオブジェクト」
何も書かなくて良い
using UnityEngine;

public class PooledObjects : MonoBehaviour {
    void Start() { }
    void Update() { }
}

Pool.cs

「プール」
ObjectPoolの本体です。
using UniRx.Toolkit;
using UnityEngine;

public class Pool : ObjectPool<PooledObjects> {
    private GameObject _prefab;

    public Pool(GameObject prefab) {
        _prefab = prefab;
    }

    protected override PooledObjects CreateInstance() {
        return GameObject.Instantiate(_prefab).GetComponent<PooledObjects>();
    }
}


スクリプトの説明
1行目
using UniRx.Toolkit;
オブジェクトプールの定義はUniRx.Toolkitというnamespaceにある。

4行目
public class Pool : ObjectPool<PooledObjects>{
のようにObjectPool<PooledObjects>を継承する

ObjectPool<T>
定義は
public abstract class ObjectPool<T> : IDisposable
        where T : UnityEngine.Component
{
        //省略
}
のようになっている。

このクラスには
protected abstract T CreateInstance();
が定義されているので、
11~13行
protected override PooledObjects CreateInstance() {
        return GameObject.Instantiate(_prefab).GetComponent<pooledobjects<();
}
を実装する。
名前の通りインスタンスを生成するメソッド。
プール内のオブジェクトが足りなくなった時に勝手に呼ばれるらしい。

ちょっと前後してしたけど5~9行目
private GameObject _prefab;

    public Pool(GameObject prefab) {
        _prefab = prefab;
    }
コンストラクタ
たくさん生成したい物(今回ではPooledObjects)を渡して、privateフィールドに保存する


Manager.cs

マネージャースクリプト。
オブジェクトプールを作って、オブジェクトの生成を行う。
using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class Manager : MonoBehaviour {
    public GameObject Prefab;
    private Pool pool;
    public static Subject ReturnPool = new Subject();

    void Start() {
        pool = new Pool(Prefab);
        this.OnDestroyAsObservable().Subscribe(_ => pool.Dispose());
        ReturnPool.Subscribe(_obj => pool.Return(_obj)).AddTo(this.gameObject);
    }
    void Update() {
        if (Input.GetMouseButtonDown(0))
            pool.Rent();
    }
}


スクリプトの説明
1,2行目
using UniRx;
using UniRx.Triggers;
UniRx名前空間は7行目のSubjectの定義に
UniRx.Triggersは11、14~18行目のクリック入力を実装するのに必要

6行目
public GameObject Prefab;
prefabにはインスペクターから量産したいGameObject(今回はPooledObjectをアタッチした物)をアウトレット接続するする

8行目
public static Subject<PooledObjects> ReturnPool = new Subject<PooledObjects>();
ReturnPoolは引数にPooledObjectsをとる「関数名の宣言だけ(delegate)」みたいな物
12行目で内容を記述している
他の場所から呼び出す(後述)のでpublic static にしておく

11行目
Pool pool = new Pool(Prefab);
プールをインスタンス化
Pool.csの7~9行目のコンストラクタで引数にGameObject型を要求しているので、量産したいオブジェクト(プレハブ)を渡す。

12行目
this.OnDestroyAsObservable().Subscribe(_ => pool.Dispose());
PoolはMonoBehaviourを継承していないので、このゲームオブジェクトが破棄(Destroy)されても、そのインスタンスが残ってしまう。
なので、このゲームオブジェクトが破棄された時にDispose()を呼んで破棄するようにする。

13行目
ReturnPool.Subscribe(_obj => pool.Return(_obj)).AddTo(this.gameObject);
7行目で宣言したReturnPoolの内容を追加(登録)する。
_objは渡されたインスタンス
これをpool.Return()することで、そのインスタンス(_obj)がSetActive(false)される。

これもUniRxのクラスなので、11行目と同様にこのゲームオブジェクトが破棄(Destroy)されても、ReturnPoolが残ってしまう。
なので最後にAddTo()して、このゲームオブジェクトが破棄されたら一緒に破棄されるようにする。

15~17行目
void Update() {
        if (Input.GetMouseButtonDown(0))
            pool.Rent();
    }
左クリックの時、pool.Rent()を呼んでオブジェクトプールから1つインスタンスを取り出す(SetActive(true))。
インスタンスが無い時はInstantiate()される。


Unityで実行する

量産するものを作り、PooledObject.csをアタッチ

マネージャーを作成してManager.csをアタッチ

実行して、左クリックすると、このように増えていきます。


まだこの段階では生成したオブジェクトをオブジェクトプールに戻すことができません。

では、生成後1秒経過すると消えるようにしてみます。

今まで何も書いていなかったPooledObjects.csを以下のように変更します。
この時、Start()を使うのではなく、OnEnable()を使ういます。
PooledObjects.cs
using UnityEngine;

public class PooledObjects : MonoBehaviour {
    void OnEnable() {
        Invoke("destroy", 1);
    }
    void destroy() {
        Manager.ReturnPool.OnNext(this);
    }
}
これで、以下のようにオブジェクトプールが完成しました。

このようにプールから取り出した後に実行したい(開始したい)処理はOnEnable()に書くのが良いのかなと思います。

実際に使っていく

ここまでで作ったオブジェクトプールを実際のゲーム作成で使うには機能が足りないと思います。
なので、Awake()、Start()、Update()に加えてOnEneble()に追加記述していくのが基本になるかと思います。

最後に参考記事等を紹介して終わります。
このページ(UniRx付属のObjectPoolが割とガチで使える件)では
Poolを空にする
あらかじめObjectを作成しておく
現在のプール数を一定まで減らす
などのUniRxのObjectPool付属の機能を紹介してくれています。
また、このQiita(UniRxのObjectPoolを利用する)ではエフェクト作成を例にプログラム全体を書いてくれています。
このQiitaでのオブジェクtプール本体(ここでのPool.csこと)ではコンストラクタの引数にTransform型も加えてとり、「ヒエラルキーが散らからないように一箇所にまとめる」ということも行っています。
私自身はunity1week「密」にて使いました。
その時のスクリプトの話をすると、
・マネージャースクリプトには入力処理などのほか量産したインスタンスをList<T>で管理などする
・量産するオブジェクトのスクリプトはAwake()にObservableを定義してそのオブジェクトのイベント処理を書き、OnEnable()に初期化処理的なことを書きいた
・オブジェクトプール本体は今回紹介したPool.csと全く同じ

自己紹介

自分の写真
県立高校理数科2年
Unity2年目
Twitter
Note

人気の投稿

[Vim] coc.nvim + coc-clangdの構文チェックでboostのエラーが発生する

C++の拡張ライブラリ、Boost。 AtCoderでも使用が認められているライブラリです。 様々な便利機能が搭載されています。 競技プログラミングで使えるBoostライブラリ 初級編 から一部抜粋すると、 boost...

QooQ