[Unity]標準ライブラリで簡単に作れるオブジェクトプール(Object Pool)

2021年5月14日(更新: 2022年2月14日)

Object PoolObject Pooling)は、頻繁に生成・破棄が行われるゲームオブジェクト(銃弾や粒子エフェクトなど)の処理負荷を軽減するための仕組みであり、ゲーム制作においてはよく使われます。

オブジェクトプールとは?

繰り返し登場するオブジェクトをいくつか破棄せずに非アクティブ化して保持しておき、それらの必要なときにアクティブ状態に切り替えて再利用します。

昔のゲームでは、画面上に一度に表示できる弾数が一発だけで、リロード時間はタイマーではなく、弾が当ったかどうかで変化するようなものが有りました。このテクニックは、object pooling と呼ばれ、メモリ管理を簡素化し、プログラムがより滑らかに実行されるようになります。
……
弾が消滅した瞬間、単純に非アクティブ化して、再び発射できるように再配置してアクティブ化します。これなら常に同じメモリ空間上に配置しておくだけでよく、移動させたり、絶えず削除して再作成する必要もありません。
スクリプトとゲームプレイ方法 – Unity マニュアル

毎回オブジェクトを作成(Instantiate)して破棄(Destroy)する場合に比べてメモリ割り当てが簡易になり、動的メモリの割り当て負荷とガベージコレクション作業を削減できるとされます。

現在は Unity の標準ライブラリ(UnityEngine.Pool)として用意されており、以前に比べて簡単に利用できるようになりました。

今回は UnityEngine.Pool によるオブジェクトプールの簡易的な使い方と実際の動作について紹介します。

ObjectPoolのサンプルシーン・コード

Unity のプロジェクト新規作成で3Dシーンを作成し、デフォルトのメインカメラに以下のコードをアタッチします。

スクリプト冒頭で using UnityEngine.Pool の一行を加えることを忘れないでください。

using UnityEngine;
// 以下を追加
using UnityEngine.Pool;

public class CreateObject : MonoBehaviour
{
    ObjectPool<GameObject> objectPool;

    void Awake()
    {
        // オブジェクトプールを作成
        objectPool = new ObjectPool<GameObject>(() =>
        {
            // 生成処理
            var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
            var pooled = cube.AddComponent<PooledObject>();
            pooled.objectPool = objectPool;
            return cube;
        },
        target =>
        {
            // 再利用処理
            print("GET");
            target.SetActive(true);
        },
        target =>
        {
            // プールに戻す処理
            print("RELEASE");
            target.SetActive(false);
        },
        target =>
        {
            // プールの許容量を超えた場合の破棄処理
            print("DESTROY");
            Destroy(target);
        }, true, 100, 1000);
    }

    void Update()
    {
        // プールから取得
        // プールのオブジェクトが足りない場合は生成
        GameObject pooledObject = objectPool.Get();
        pooledObject.transform.position = transform.position + transform.forward * 20 + Random.onUnitSphere * 5;
        pooledObject.transform.rotation = Random.rotation;

        // オブジェクトプールを使わない場合の通常の生成
        // Instantiate(pooledObject, transform.position + transform.forward * 20 + Random.onUnitSphere * 5, Random.rotation);
    }

    void OnGUI()
    {
        // 情報表示
        GUILayout.Label("Pool Size: " + objectPool.CountInactive);
        GUILayout.Label("Active Size: " + objectPool.CountActive);
    }
}

ObjectPool のコンストラクタは以下の引数を受け取ります。

public ObjectPool<T0> (
    Func<T> createFunc, 
    Action<T> actionOnGet = null, 
    Action<T> actionOnRelease = null, 
    Action<T> actionOnDestroy = null, 
    bool collectionCheck = true, 
    int defaultCapacity = 10, 
    int maxSize = 10000
);
  1. createFunc:生成を行う関数
  2. actionOnGet:プールから取得した時の処理
  3. actionOnRelease:プールに入れた時の処理
  4. actionOnDestroy:プールの許容量を超えた時の削除処理
  5. collectionCheck:既にプールにある場合に報告する(例外を投げる)かどうか(UnityEditorでのみ有効)
  6. defaultCapacity:初期の許容量
  7. maxSize:最大許容量

今回はプールに入れるオブジェクトとしてプリミティブのCubeを生成しています。

そのオブジェクトには以下のコードをアタッチするようにします(上記のコードの17行目でこれを行っています)。

using UnityEngine;
using UnityEngine.Pool;

public class PooledObject : MonoBehaviour
{
    public ObjectPool<GameObject> objectPool;
    float time;

    void Update()
    {
        if (time > 1)
        {
            time = 0;

            // オブジェクトをプールに入れる
            objectPool.Release(gameObject);

            // オブジェクトプールを使わない場合の通常の破棄
            // Destroy(gameObject);
        }
        time += Time.deltaTime;
    }
}

このコードによって、生成から1秒後にオブジェクトがオブジェクトプールに入れられて再利用可能となります。

オブジェクトプールの実行例

オブジェクトを再利用できるObjectPoolの例

初めはオブジェクトプールに利用できるオブジェクトがないため createFunc によってオブジェクトが新しく生成されます。

その後、オブジェクトが Release によってプールに再利用できるオブジェクトができた時は生成は止まり、非アクティブ化されたオブジェクトをアクティブにすることで再利用されます。

プールに入るオブジェクトは非アクティブに切り替わるため、画面から消えます。

初めからオブジェクトプールのものを使用したいのであれば、起動時にある程度プールにオブジェクトを入れておくと良いでしょう。

第五引数(collectionCheck)のブール値を true とすることで、一度プールに開放したオブジェクトを再度プールに入れようとすると以下の例外(エラー)が出力されるようにできます。無駄な開放を特定するのに役立つかもしれません。

InvalidOperationException: Trying to release an object that has already been released to the pool.

汎用的なクラスである GameObject ではなく、特定のクラス(をアタッチしたオブジェクト)をプールの対象とする場合は以下のように書けます。

ObjectPool<Enemy> enemyPool = new ObjectPool<Enemy>(
    () => Instantiate(enemyPrefab).GetComponent<Enemy>(),
    target => target.gameObject.SetActive(true),
    target => target.gameObject.SetActive(false),
    target => Destroy(target.gameObject), true, 30, 30
);

以上、オブジェクトプールの簡易的な使用例でした。

オブジェクトプールを使わない方が良い場合?

大量にゲーム中に登場するオブジェクトでも、オブジェクトプールによる管理が最適ではないこともあります。

例えば、最初に大量にプールしたオブジェクトが徐々に使われなくなっていく状況などです。

大きすぎるプールを割り当てたり、プールに含まれているオブジェクトがしばらく不要であるにもかかわらずプールをアクティブにしている場合に、明らかにパフォーマンスが悪くなることがあります。さらに、オブジェクトプールに向かない種類のオブジェクトも多くあります。
例えば、長時間持続する魔法のエフェクトや、大量に出現し、ゲームの進行とともに徐々に倒されていく敵がゲームに含まれている場合がなどです。このようなケースでは、オブジェクトプールによるパフォーマンスのオーバーヘッドのほうが利点を大幅に上回るので、オブジェクトプールは使用すべきではありません。
スクリプトの最適化 – Unity マニュアル

生成と削除の頻繁な繰り返しによるオーバーヘッドを抑えるというのがオブジェクトプーリングを行うメリットとなります。

ほとんど再登場しないオブジェクトを大量にプールしてしまうと、逆にパフォーマンスが低下する恐れがあります。

ライブラリの詳細は公式ドキュメントを御覧ください。

Unity – Scripting API: ObjectPool<T0>

コメントを残す

メールアドレスが公開されることはありません。