【Unity】オブジェクトプールとは?大量のオブジェクトを扱う仕組み

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

こんにちは、ともくんのゲーム作り部屋にようこそ!

このページでは、

「Unityのオブジェクトプールってなに?」

「オブジェクトプールはどうやって使うの?」

というお悩みの方に向けた内容となっています。

Unityでは、たくさんのオブジェクトを扱う際にオブジェクトプールというシステムが使われることがあります。

このオブジェクトプールとは、生成したオブジェクトが不要になった際に、削除せずに非アクティブの状態で保存しておき、再度使いたい時にアクティブにして利用する仕組みのことです。

オブジェクトプールを使うことで、大量のオブジェクトを扱う際に何度もオブジェクトの生成や削除を繰り返す必要がなくなるので、ゲーム内の負担を軽減させることができます。

そこでこのページでは、Unityのオブジェクトプールについて、どういうものなのか、また使い方までをまとめていきます。

この記事を書いた人

ゲーム作りを学び始めた一児のパパです。
このブログは、子供から「ゲームを作ってみたい!」と言われ、非プログラマーでゲーム作りをしたことない僕が、ゲーム作りの本を読んで独学でゲーム開発を学んでいるブログです。
同じように初めてゲーム作りをしている方と一緒に学んでいけるようなブログに出来たらいいなと思っています。
また、「このコードはおかしい」とか「もっと良い書き方があるよ!」などあれば、どんどん指摘して頂けると助かります。

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

まずは、オブジェクトプールがどういった仕組みのものなのかについて紹介していきます。

大量のオブジェクトを再利用で管理できる仕組み

オブジェクトプールは、大量のオブジェクトを扱う際に使われるシステムで、ゲーム内で使い終わったオブジェクトを保存しておき、再度利用できるようにしておく仕組みのことです。

もう少し詳しく言うと、生成したオブジェクトをオブジェクトプールの中で非アクティブで保存させておき、利用する際にオブジェクトを取り出してアクティブにして使います。

そして、そのオブジェクトを使い終わったら、再度非アクティブにしてオブジェクトプールに保存しておくことで、再利用できるようにしているのがオブジェクトプールの役割です。

例えば、敵オブジェクトを複数体表示させて、それを倒していくようなゲームを作る場合に、オブジェクトプールを使うことで、倒した敵オブジェクトを再利用して表示させるということができるようになります。

他にも、何度も表示させるようなパーティクスシステムで作るエフェクトなども、オブジェクトプールで扱うことができるので便利です。

ゲーム内の負担軽減に繋がる

オブジェクトプールを使うメリットとして、ゲーム内の負担を軽減させて、ゲームが遅くなったりする原因を取り除くことができます。

オブジェクトの生成処理を行うInstantiateメソッドや、オブジェクトの破壊処理を行うDestroyメソッドは、簡単に使える半面、ゲーム内で何度も繰り返すと負荷がかかり、ゲームが止まってしまうことがあります。

特に、大量のオブジェクトを扱うようなゲームを作る場合に、オブジェクトが出てくる度にInstantiateとDestroyで生成と破棄を繰り返すのは、非効率でゲーム内の負担に繋がります。

そこで、オブジェクトプールを使うと、生成されたオブジェクトをプールの中で保存させて再利用することができるので、オブジェクトの生成と破棄の処理を繰り返すことがなくなります。

そのため、大量のオブジェクトを扱う際に、オブジェクトプールを使うことでゲーム内の負担を軽減させる効果があります。

オブジェクトプール(ObjectPool)の使い方

ここからは、オブジェクトプールの使い方について紹介していきます。

オブジェクトプールの基本的な使い方

オブジェクトプールの基本的な使い方として、

  • オブジェクトプールの本体を作る
  • オブジェクトプールからオブジェクトを取り出す
  • 不要なオブジェクトをオブジェクトプールに返却する

という3つの流れで紹介していきます。

オブジェクトプールの本体を作る

ここでは、オブジェクトを管理するオブジェクトプールの本体を作っていきます。

サンプルとして、以下のスクリプトを元に説明していきます。

using UnityEngine;
using UnityEngine.Pool;     // オブジェクトプールを利用する際に必要

public class ObjectPool : MonoBehaviour
{
    ObjectPool<GameObject> pool;    // オブジェクトプールの変数を宣言
    public GameObject obj;  // プールの中で管理したいオブジェクト

    void Start()
    {
        // オブジェクトプールのインスタンスを生成
        pool = new ObjectPool<GameObject>(
            CreatePooledItem,       // オブジェクト生成の際の処理
            OnTakeFromPool,         // オブジェクトを取り出す際の処理
            OnReturnedToPool,       // オブジェクトを返却する際の処理
            OnDestroyPoolObject,    // プールが上限を超えた場合の処理
            true,                   // すでにプール内にいるオブジェクトを返却した際にエラー表示するか
            2,                      // 初期のプールの容量
            10);                    // プール内オブジェクトの上限数
    }

    // オブジェクト生成の際の処理
    GameObject CreatePooledItem()
    {
        return Instantiate(obj);    // オブジェクトを生成してプールに渡す処理
    }

    // オブジェクトを取り出す際の処理
    void OnTakeFromPool(GameObject obj)
    {
        obj.SetActive(true);    // オブジェクトをアクティブにする処理
        obj.transform.position = new Vector2(Random.Range(-8f, 8f), Random.Range(-4.5f, 4.5f));  // オブジェクトの座標を指定する処理
    }

    // オブジェクトを返却する際の処理
    void OnReturnedToPool(GameObject obj)
    {
        obj.SetActive(false);   // オブジェクトを非アクティブにする処理
    }

    // プールが上限を超えた場合の処理
    void OnDestroyPoolObject(GameObject obj)
    {
        Destroy(obj.gameObject);    // オブジェクトを破壊する処理
    }
}

まず、オブジェクトプールを作るクラスには、スクリプトの上部の2行目のように、

using UnityEngine.Pool;

という記述をしておく必要があります。

そして、ObjectPool型の変数を6行目で宣言しています。

ObjectPool<クラスの型名> オブジェクトプールの変数名;

クラスの型は、オブジェクトプールで扱いたい型のことで、ゲームオブジェクトであれば「GameObject」と指定すれば問題ありません。

この変数の中はまだ空っぽなので、Startメソッドの中の11行目から19行目でnew演算子を付けてObjectPool型のインスタンスを生成しています。

インスタンスを生成する際に、ObjectPool型では引数で複数のコンストラクタが設定されているため、指定していく必要があります。

ObjectPool pool = new ObjectPool<クラスの型>(
	CreatePooledItem,
	OnTakeFromPool,
	OnReturnedToPool,
	OnDestroyPoolObject,
	collectionChecks,
	defaultCapacity,
	maxPoolSize);

なお、それぞれの引数に指定している名前は異なるもので問題ありません。

まず、第1引数となるCreatePooledItemは、オブジェクトプールの中でオブジェクトを生成する際に行われる処理となる部分で、プールの中が空になって足りなくなった時に行われます。

CreatePooledItemの処理内容は、23行目から26行目の部分で記述していて、この中でオブジェクトを生成するInstantiateの処理を行い、生成されたオブジェクトをプールの中で管理できるようにしています。

注意点として、このCreatePooledItemのメソッドは、戻り値をオブジェクトプールで指定したクラスの型であるGameObject型としていて、returnを使って生成したオブジェクト情報を返すことで、そのオブジェクトがプールの中に保存される仕組みになっています。

第2引数となるOnTakeFromPoolは、オブジェクトプールから取り出した時に行う処理となる部分で、オブジェクトをアクティブ化させたり、オブジェクトの座標位置を変更させる処理などを行うことになります。

このOnTakeFromPoolの処理は、29行目から33行目の部分に書いていて、引数にGameObject型の変数を指定しておくことで、取り出されるオブジェクトの情報を受け取ることができます。

その変数を使って、SetActiveメソッドでアクティブ化したり、座標を指定する処理などを記述しておきます。

第3引数となるOnReturnedToPoolは、先ほどとは反対にオブジェクトを使い終わってオブジェクトプールに戻すときの処理となる部分で、主にそのオブジェクトを非アクティブ化する処理がメインとなってきます。

OnReturnedToPoolの処理は、36行目から39行目の部分となっていて、引数で返却するオブジェクトの情報を受け取れるので、SetActiveメソッドを使って非アクティブ化しています。

第4引数となるOnDestroyPoolObjectは、オブジェクトプールの上限数を超えてオブジェクトが生成されている場合に、上限を超えているオブジェクトに対して行われる処理となる部分で、基本的にはオブジェクトを破壊する処理を行うことになります。

OnDestroyPoolObjectは、42行目から45行目の部分で、他と同様に引数で上限を超えているオブジェクトの情報を受け取れるので、このオブジェクトをDetroyメソッドで破壊するようにしています。

第5引数となるcollectionChecksは、同じオブジェクトがプール内にある状態で、プールに戻す処理が行われた場合に、エラー表示をさせるかどうかを決めている部分で、bool型で値を指定してあげます。

第6引数となるdefaultCapacityは、オブジェクトプールの容量のデフォルトサイズを決めている部分で、int型でデフォルトの数を指定します。

第7引数となるmaxPoolSizeは、オブジェクトプールの容量の上限となるサイズで、defaultCapacityと同じくint型で上限の数を指定します。

このmaxPoolSizeの上限を超えたオブジェクトは、前述のOnDestroyPoolObjectの処理が行われることになります。

引数が非常に多いですが、ここまでで、オブジェクトプールの本体を作成することができました。

オブジェクトプールからオブジェクトを取り出す

次は、作成したオブジェクトプールから実際にオブジェクトを取り出す処理を行えるようにしていきます。

オブジェクトプールからオブジェクトを取り出す処理は、ObjectPool型で定義されているGetメソッドを使います。

ObjectPool.Get();

ただし、このGetメソッドは他のクラスから直接アクセスすることができないため、オブジェクトプールの中でアクセサを作って、他のクラスから処理を行えるようにしておきます。

using UnityEngine;
using UnityEngine.Pool;

public class ObjectPool : MonoBehaviour
{
    //  一部省略 //

    void OnDestroyPoolObject(GameObject obj)
    {
        Destroy(obj.gameObject);
    }

    // 他のクラスからオブジェクトを取り出せるようにするためのメソッド
    public GameObject GetObject()
    {
        return pool.Get();  // プールからオブジェクトを取り出す処理
    }
}

14行目から17行目で、public修飾子を付けてGetObjectというメソッドを作っていて、その中にGetメソッドの処理を記述しています。

これで他のクラスからGetObjectのメソッドを使うことで、プールからオブジェクトを取り出すことができるようになります。

実際に、空オブジェクトを作成して、以下のスクリプトでマウスをクリックしたらオブジェクトが表示されるように処理を記述しておきます。

using UnityEngine;

public class ObjectGenerater : MonoBehaviour
{
    public ObjectPool pool; // オブジェクトプールを取得するための変数

    void Update()
    {
        // マウスをクリックした場合
        if (Input.GetMouseButtonDown(0))
        {
            pool.GetObject();   // プールからオブジェクトを取り出す処理を呼び出す
        }
    }
}

5行目でObjectPool型の変数をpublicで宣言して、インスペクターウィンドウからオブジェクトプールを代入しておきます。

そして、マウスをクリックした処理の中の12行目で、GetObjectメソッドを記述しています。

これでゲームを実行してマウスをクリックしてみると、

オブジェクトがどんどん生成されているのが分かります。

不要なオブジェクトをオブジェクトプールに返却する

あとは、オブジェクトが使い終わったら、オブジェクトプールに返却する処理を記述しておきます。

このオブジェクトプールに返却する処理は、ObjectPool型で定義されているReleaseメソッドを使います。

ObjectPool.Release(返却するオブジェクト);

先ほどのGetメソッドと同じく、以下のように14行目から17行目でReleaseObjectというアクセサを作っておきます。

using UnityEngine;
using UnityEngine.Pool;     // オブジェクトプールを利用する際に必要

public class ObjectPool : MonoBehaviour
{
    // 一部省略 //

    public GameObject GetObject()
    {
        return pool.Get();
    }

    // 他のクラスからオブジェクトを返却できるようにするためのメソッド
    public void ReleaseObject(GameObject obj)
    {
        pool.Release(obj);  // プールにオブジェクトを返却する処理
    }
}

どのオブジェクトをプールに返却するかは、Releaseメソッドの引数でオブジェクトを指定する必要があるので、アクセサでも同様に引数を受け取れるようにしておきます。

オブジェクトの返却タイミングは、オブジェクトを取り出して表示させてから、1回転した後に返却するようにしたいので、オブジェクトに紐づけたスクリプトに以下のように記述していきます。

using UnityEngine;

public class ObjectController : MonoBehaviour
{
    public ObjectPool pool; // オブジェクトプールを取得するための変数
    int count;

    void Update()
    {
        transform.Rotate(0, 0, 1f); // 回転処理
        count++;    // 回転したカウント数を計測

        if (count > 360)
        {
            pool.ReleaseObject(gameObject); // オブジェクトを返却する処理
            count = 0;  // カウント数を初期化
            transform.rotation = Quaternion.identity;   // 回転量を初期化
        }
    }
}

10行目でRotateメソッドを使ってオブジェクトを回転する処理を行っていて、一回転させた後の15行目でReleaseObjectを記述して、そのオブジェクトをプールに返却する処理を行っています。

これでゲームを実行して再度マウスをクリックしてみると、

オブジェクトが表示されて回転処理が終わると、非表示となっているのが分かります。

注目すべきはヒエラルキーウィンドウの部分で、非表示となったオブジェクトが再度使用されており、オブジェクトプールを使うことで、何度も生成と破棄の処理が行われなくなっているのが分かります。

大量の敵をオブジェクトプールで管理する

実際に、オブジェクトプールをUnityで使う場合、Prefabオブジェクトを生成して作ることが多くなりますが、Prefabからオブジェクトプールを参照するのに、シングルトンという仕組みを使う方が便利です。

シングルトンとは、オブジェクトプールのインスタンスを1つしか生成されないようにすることで、他のクラスからアクセスしやすくなるようにする仕組みのことです。

そこで、ここでは、大量の敵を管理するオブジェクトプールを作り、それをシングルトンの仕組みで作成していきます。

まず、以下のように敵オブジェクトと円形のオブジェクトを配置しておきます。

そして、敵オブジェクトに対して、以下のスクリプトをアタッチしておきます。

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    Vector3 circlePosition;
    Vector3 velocity = Vector3.zero;
    float moveTime = 1.5f;

    void Start()
    {
        circlePosition = new Vector2(0, -3.1f);
    }

    void Update()
    {
        transform.position = Vector3.SmoothDamp(transform.position, circlePosition, ref velocity, moveTime); // 円形のオブジェクトに向かって進んでいく処理
    }

    // 衝突判定が起きた場合
    void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.name == "Circle")
        {
            gameObject.SetActive(false);    // 非アクティブ化する処理
        }
    }
}

敵のオブジェクトは、円形のオブジェクトに向かって進むように、16行目でSmoothDampメソッドを使って移動させています。

そして、20行目でOnTriggerEnter2Dメソッドの中で、円形オブジェクトと衝突した場合に非アクティブになるように処理しています。

実際に動かしてみると、

上記のような動きになります。

この敵オブジェクトを大量に複製していくことになるので、Prefab化しておきます。

次に、オブジェクトプールを作っていくので、空オブジェクトを作成して、以下のEnemyManagerというスクリプトをアタッチしていきます。

using UnityEngine;
using UnityEngine.Pool;

public class EnemyManager : MonoBehaviour
{
    // シングルトンの作成
    public static EnemyManager instance;    // EnemyManager型の変数を宣言

    void Awake()
    {
        // インスタンスが入っていない場合
        if (instance == null)
        {
            instance = this;    // インスタンス自身を入れる処理
        }
        // インスタンスがすでに入っている場合
        else
        {
            Destroy(this.gameObject);   // インスタンスに紐づくオブジェクトを破壊
        }
    }

    // オブジェクトプールの作成
    ObjectPool<GameObject> pool;    // ObjectPool型の変数を宣言
    public GameObject enemyPrefab;  // オブジェクトプールで管理するオブジェクトを指定

    void Start()
    {
        // オブジェクトプールのインスタンスを生成する
        pool = new ObjectPool<GameObject>(
            CreateEnemy,
            OnGetEnemy,
            OnReturnEnemy,
            OnDestroyEnemy,
            false,
            2,
            20);
    }

    GameObject CreateEnemy()
    {
        return Instantiate(enemyPrefab);    // Prefabオブジェクトを生成する処理
    }

    void OnGetEnemy(GameObject obj)
    {
        obj.SetActive(true);    // オブジェクトをアクティブにする処理
        obj.transform.position = new Vector2(Random.Range(-8f, 8f), Random.Range(-3.5f, 4.0f)); // オブジェクトの座標位置を指定する処理
    }

    void OnReturnEnemy(GameObject obj)
    {
        obj.SetActive(false);   // オブジェクトを非アクティブ化する処理
    }

    void OnDestroyEnemy(GameObject obj)
    {
        Destroy(obj);   // オブジェクトを破壊する処理
    }

    // 他のクラスから敵を取り出すための処理
    public void GetEnemy()
    {
        pool.Get();
    }

    // 他のクラスから敵をプールに戻すための処理
    public void ReleaseEnemy(GameObject obj)
    {
        pool.Release(obj);
    }
}

ここではシングルトンでオブジェクトプールを作成したいので、7行目で「public static」を頭に付けてEnemyManager型の変数を作成しておきます。

そして、Awakeメソッドの中で、変数にインスタンスが入っていなければ14行目でインスタンス自身を入れてあげて、すでに入っている場合は19行目でインスタンスを削除することで、必ずインスタンスが一つになるようにしています。

これで他のクラスからは、「EnemyManager.instance.●●」と書くことで、簡単にオブジェクトプールの変数やメソッドにアクセスすることができるようになります。

シングルトンができたらオブジェクトプールの本体を作っていくので、24行目でObjectPool型の変数を宣言して、30行目でインスタンスを引数付きで生成しています。

インスタンス生成の引数には、

  • CreateEnemy:Prefabの敵オブジェクトを生成する処理(40行目)
  • OnGetEnemy:敵オブジェクトをアクティブ化して座標をランダムに配置(45行目)
  • OnReturnEnemy:敵オブジェクトを非アクティブ化する処理(51行目)
  • OnDestroyEnemy:敵オブジェクトを破壊する処理(56行目)
  • collectionChecks:「false」を指定
  • defaultCapacity:「2」を指定
  • maxPoolSize:「20」を指定

をそれぞれ設定しています。

また、62行目で他のクラスからオブジェクトを取り出すためのGetEnemyメソッド、68行目で他のクラスからオブジェクトを返却するためのReleaseEnemyメソッドをpublic修飾子を付けて記述しています。

あとは、敵を自動で生成するためのスクリプトを作成して、空オブジェクトにアタッチしておきます。

using UnityEngine;

public class EnemyGenerator : MonoBehaviour
{
    float time = 0.5f;  // 敵を表示させる間隔
    float delta;

    void Update()
    {
        delta += Time.deltaTime;
        if (time < delta)
        {
            EnemyManager.instance.GetEnemy();   // プールから敵を取り出す処理
            delta = 0;
        }
    }
}

今回は、0.5秒に1回敵が自動で生成されるようにしたいので、Time.deltaTimeで0.5秒計測されたら、13行目でオブジェクトプールで作成したGetEnemyメソッドの処理が行われるようにしています。

また、最初に作成した敵オブジェクトにアタッチしたスクリプトにも、オブジェクトプールに返却する処理を加えておきます。

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    // 一部省略 //

    void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.name == "Circle")
        {
            EnemyManager.instance.ReleaseEnemy(gameObject);   // プールに敵を返却する処理
        }
    }
}

円形のオブジェクトと衝突した際の11行目で、ReleaseEnemyメソッドを使ってオブジェクトプールに返却する処理を記述しています。

これでゲームを実行してみると、

0.5秒に1回敵が生成されて、円形のオブジェクトに衝突すると非アクティブ化していますが、次の敵の生成の際に再利用して使われているのが分かります。

まとめ

このページでは、Unityで使えるオブジェクトプールについて、どんな仕組みなのか、使い方までをまとめていきましたが、いかがでしたでしょうか?

オブジェクトプールとは、大量のオブジェクトを管理することができるシステムで、使わなくなったオブジェクトを再利用することができます。

このオブジェクトプールを使うことで、オブジェクトの生成や破棄を繰り返す必要がないので、ゲーム内の負担を軽減させることができます。

また、オブジェクトプールを作る際は、シングルトンで作成しておくと、アクセスが非常にしやすく便利になります。

最後までお読みいただきまして、ありがとうございました!

コメント