5
1

More than 3 years have passed since last update.

【Unity・C#】軽量なObjectPoolを作りたい

Last updated at Posted at 2021-05-14

やりたいこと

ObjectPoolingは大量のオブジェクトを扱う際、メモリ割り当てをしないように一度生成したオブジェクトを常に使い回すための仕組みです。

しかしObjectPoolは結構再開発されまくってて、目的・用途が様々になってきています。
なので、ここで一旦要件をまとめておきたいと思います。

・プールされるオブジェクトはGameObjectのComponentである。
・GameObjectには必ず同じComponentがアタッチされている。
・プールされるオブジェクトは個体識別しない。
・プールされるオブジェクトに生成時、破棄時のコールバックが欲しい
・プールはシングルトン
・例外なくちゃんとエラーを吐いてくれる

コードはGithubにあげています。

実装

オブジェクトプール

プールするための配列はQueue<T>を使用しました。

CreatePool(int)で許容量を設定しつつプールを生成します。
ここで許容量を設定するのは、許容量を超えた際にメモリの再割り当てが発生するのを防ぐためです。
また、普通のQueueとは少し違いプールの許容量を超えたら、再割り当てするかしないかをisFixedで設定することができます。(普通は自動で再割り当てされてしまう)

        public void CreatePool(int capacity, bool isFixed) {
            if (m_isActive) {
                Debug.LogWarning("Poolは既に生成されています!");
                return;
            }

            if (prefab == null) {
                Debug.LogError("Prefabがセットされていません!");
                return;
            }

            m_capacity = capacity;
            Pool = new Queue<IPoolable<T2>>(capacity);

            for (int i = 0; i < capacity; i++) {
                T2 obj = Instantiate(prefab, transform).GetComponent<T2>();
                obj.gameObject.SetActive(false);
                Pool.Enqueue(obj);
            }

            m_isActive = true;
            m_isFixed = isFixed;
        }

Release()でプールからオブジェクトを取り出すことができます。
基本的に余計なことはしないスタンスなので、コールバックを呼び出しオブジェクトを取り出すだけの実装になっています。
ここで例外をキャッチしているのは、Pool.Dequeue()でプールが空の状態の時に例外を発生させるからです。ドキュメントによると、TryDequeue(out T)が使えるらしいのですが、恐らくバージョン的に存在しなかったため例外処理をしています。

        public T2 Release() {
            if (!PoolIsAvailable()) return null;

            try {
                IPoolable<T2> obj = Pool.Dequeue();
                T2 entity = obj.Entity;

                entity.gameObject.SetActive(true);
                obj.OnReleased();

                return entity;
            } catch {
                Debug.LogError("Release可能なPoolEntityが存在しません!");
                return null;
            }
        }

Catch(T)でコールバックを呼び出し、プールにオブジェクトを返却します。
IsFixedを指定した場合、CanDequeue()で弾かれて追加されるのを防ぎます。

        public void Catch(IPoolable<T2> obj) {
            if (!PoolIsAvailable()) return;
            if (!CanDequeue()) return;

            obj.OnCatched();
            obj.Entity.gameObject.SetActive(false);
            Pool.Enqueue(obj);
        }

プールされるオブジェクト

プールされるComponentにはIPoolable<T>の実装を強制します。
IPoolable<T>の実装はこのようになっています。

    public interface IPoolable<T> where T : MonoBehaviour {
        T Entity { get; }

        void OnReleased(); //生成時(取得)
        void OnCatched();  //破棄時(返却)

    }

EntityはIPoolableを実装するクラスのインスタンスを返させます。
OnReleased(), OnCatched()はそれぞれ生成時、破棄時に呼び出されます。
(キャッチアンドリリースってわかりやすい)

使い方

ObjectPool<継承するクラス, プールするクラス>を継承して、利用することができます。
CreatePool(capacity, isFixed)でプールを作成し、DestroyPool()でプールを丸ごと破棄できます。

public class HogePool : ObjectPool<HogePool, Hoge> {
    private void Start() {
        CreatePool(100, true);
    }
}

取得と返却は先ほど言った通り、Release(), Catch(T)でできます。

            HogePool.Instance.Release();
            HogePool.Instance.Catch(this); //返却するクラスのインタンスを渡す

サンプルもリポジトリ内にあるので、活用してみてください。

まとめ

・そんなに多機能でもなく、Queue<T>を使ってるのでパフォーマンス的には問題ないはず、、?
・返却はCatch(IPoolable<T>)でinterfaceを渡している。暗黙的なキャストが発生しているので、ここがネック。
Queue<T>を自作クラスでもっと軽量にすれば軽くできるかなと思った。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1