【Unity】複数のオブジェクトプールを効率的に管理する方法

複数のオブジェクトプールを管理 Unity

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

このページでは、

「複数のオブジェクトをプールで管理するにはどうするの?」

「複数のオブジェクトプールをもっと効率的に管理したい!」

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

Unityでは、オブジェクトプールという機能を使うことで、不要となったオブジェクトを再利用する仕組みを作ることができ、これによりゲームの負荷を減らすことができます。

ただし、このオブジェクトプールを使う場合、1つのプールの中では1つのオブジェクトを管理することになるため、ゲーム内で複数のオブジェクトを再利用したいような場合は、そのオブジェクトの数だけオブジェクトプールを準備する必要があります。

この複数のオブジェクトプールを作る場合、それぞれのプールを別クラスで管理するのではなく、全てのプールをまとめたクラスを作って、その中でそれぞれのプールを管理することで効率的に作ることができます。

そこで、このページでは、Unityで複数のオブジェクトプールを作成する場合に、より効率的に管理していく方法についてまとめていきます。

開発環境
  • Windows11
  • Unity 6.3 LTS(6000.3.8f1)
この記事を書いた人

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

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

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

オブジェクトを再利用できる仕組み

オブジェクトプールとは、使い終わったオブジェクトをプールの中で管理しておき、再利用できるようにする仕組みのことです。

もう少し詳しく言うと、まずオブジェクトが不要となったタイミングで、ステータスを非アクティブ化して保管場所となるオブジェクトプールに入れておきます。

そして、そのオブジェクトを再度利用する際に、オブジェクトプールからオブジェクトを取り出してアクティブ化してあげることで、ゲーム画面に表示して再利用させることができます。

このようなオブジェクトプールを使うメリットとして、オブジェクトの生成や破棄といったゲームの負荷となる処理を減らすことができます。

そのため、ゲーム内で何度も表示させるようなもの、例えばエフェクトなどのオブジェクトは、このオブジェクトプールで管理することが多いです。

なお、基本的なオブジェクトプールの仕組みや作り方は、以下の記事にもまとめていますので、参考にしてみてください。

オブジェクト毎にプールを作る必要がある

オブジェクトプールは、オブジェクトを再利用することができますが、冒頭でも解説した通りで、1つのプールの中で1つのオブジェクトを管理することになります。

もう少し詳しく言えば、複数のオブジェクトを1つのプールで管理することができないため、オブジェクトの数だけプールを作る必要があります。

もし、1つのオブジェクトプールを作るだけであれば、空のオブジェクトを1つ作成して、そのクラス内でオブジェクトプールを作れば問題ありません。

しかし、複数のオブジェクトプールを作る場合、それぞれのクラスを分けてしまうと、オブジェクトプールの数だけクラスが必要になり複雑化していきます。

実際に僕自身が作っていたゲームで、複数のオブジェクトプールを別クラスで管理したことで、設計がややこしくなってしまったことがあります。

そこで、この後紹介する全てのオブジェクトプールを一括で管理するクラスを作り、その中で複数のオブジェクトプールを作って管理していくのが効率的です。

複数のオブジェクトプールを効率的に管理する

ここからは、複数のオブジェクトプールを一つのクラスで管理していく方法について紹介していきます。

ここでは実際に、以下の3つのコインのオブジェクトにおけるプールを作って、それぞれのオブジェクトを再利用できるようにしてみます。

ちなみに最終的に作るものとして、画面内に設置したEnterボタンを押したら入力した数字の数だけ、コインのオブジェクトを再利用して表示されるという処理を作ってみます。

オブジェクトの動きを作りPrefab化する

まずは、それぞれのコインのオブジェクトをシーン内に配置して、以下のスクリプトをアタッチしておきます。

using UnityEngine;

public class CoinController : MonoBehaviour
{
    Rigidbody2D rb;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();   // Rigidbody2Dコンポーネントを取得する
    }

    // オブジェクトがゲーム画面に表示された場合
    void OnBecameVisible()
    {
        rb.AddForce(new Vector3(Random.Range(-0.5f, 0.5f), 1.0f, 0) * 500f);   // 上向きの力を加える
    }

    // オブジェクトがゲーム画面から消えた場合
    void OnBecameInvisible()
    {
        gameObject.SetActive(false);    // 非アクティブ化する処理(仮)
    }
}

このスクリプトの中では、Rigidbody2Dコンポーネントを使って、コインが画面に表示されたら上向きに力が加わるように、15行目でAddForceメソッドの処理を記述しています。

そして、反対にコインが画面から消えたら、21行目でSetActiveメソッドを使って非アクティブ化する処理を行っています。

この非アクティブ化する処理は、後ほどプールに戻す処理に変更することになるため、一旦仮の処理として書いています。

また、全てのコインにインスペクターウィンドウからRigidbody2Dコンポーネントを付けておき、コイン毎に重さ(mass)の値を変えておきます。

現時点では、

上記のように、それぞれのコインが表示されたら上に上がった後、重力で下に落ちていき、非アクティブ化されるようになっています。

動きを作れたら、それぞれのオブジェクトをPrefab化しておきます。

複数のプールを管理するクラスを作る

次に、今回のメインとなる複数のオブジェクトプールを管理するクラスを作っていきます。

空オブジェクト(PoolManager)を作成して、以下のスクリプトを紐づけておきます。

using UnityEngine;
using UnityEngine.Pool;
using System.Collections.Generic;

public class PoolManager : MonoBehaviour
{
    // シングルトンで作成する
    public static PoolManager instance;

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(this.gameObject);
        }
    }

    // プールの種類を定義する型
    public enum PoolType
    {
        Coin1,
        Coin10,
        Coin100
    }

    // 各プールで持たせたい変数をまとめたクラス
    [System.Serializable]
    public class PoolContent
    {
        public PoolType type;
        public GameObject obj;
    }

    // インスペクターから複数のPoolContentを管理
    [SerializeField] List<PoolContent> contents;

    // PoolTypeをキーにしてオブジェクトプールにアクセスさせる
    Dictionary<PoolType, ObjectPool<GameObject>> pools = new Dictionary<PoolType, ObjectPool<GameObject>>();

    void Start()
    {
        foreach (var content in contents)
        {
            // それぞれのオブジェクトプールを作成する
            ObjectPool<GameObject> pool = new ObjectPool<GameObject>(
                () => Instantiate(content.obj),
                (obj) => OnGetObj(obj),
                (obj) => obj.SetActive(false),
                (obj) => Destroy(obj),
                true,
                2,
                20);

            // Dictionaryに登録する
            pools.Add(content.type, pool);
        }

        InitialStack(); // 初期時点におけるスタックの生成処理
    }

    // 20個ずつオブジェクトを生成
    void InitialStack()
    {
        GameObject[] obj = new GameObject[20];

        foreach (var content in contents)
        {
            for (int i = 0; i < 20; i++)
            {
                obj[i] = Instantiate(content.obj);
            }
            for (int i = 0; i < 20; i++)
            {
                pools[content.type].Release(obj[i]);
            }
        }
    }

    // オブジェクトを取得した際の処理
    void OnGetObj(GameObject obj)
    {
        obj.SetActive(true);
        obj.transform.position = Vector3.zero;
    }

    // 外部のクラスからオブジェクトを取得するためのメソッド
    public void OnGet(PoolType type)
    {
        pools[type].Get();
    }

    // 外部のクラスからオブジェクトを返却するためのメソッド
    public void OnRelease(PoolType type, GameObject obj)
    {
        pools[type].Release(obj);
    }
}

以下で、このスクリプト内の記述内容をそれぞれ分割して説明していきます。

シングルトンで作成する

まず8行目から20行目の部分で、このPoolManagerのクラスをシングルトン化して、他クラスからアクセスしやすいようにしておきます。

// シングルトンで作成する
public static PoolManager instance;

void Awake()
{
    if (instance == null)
    {
        instance = this;
    }
    else
    {
        Destroy(this.gameObject);
    }
}

シングルトンとは、クラスのインスタンスが必ず一つしか生成されないようにする設計のことで、クラス内で定義した変数やメソッドを他のクラスからアクセスするのが簡単になります。

実際に、他のクラスからアクセスする場合は、「PoolManager.instance.〇〇」と記述することで、変数やメソッドにアクセスできるようになります。

複数のオブジェクトプールを管理する場合は、様々なクラスからアクセスする必要が出てくるため、シングルトン化しておいた方が便利になるかと思います。

プール内で管理するオブジェクトをenumで定義

次に23行目から28行目で、それぞれのオブジェクトプールで管理したいオブジェクトの種類enum型を使って定義しておきます。

// プールの種類を定義する型
public enum PoolType
{
    Coin1,
    Coin10,
    Coin100
}

今回は、3つのオブジェクトを管理したいので、「PoolType」という型名で3つの定数を定義しています。

ちなみに、他クラスからもこのPoolTypeを使って指定することになるので、頭にpublicを付けておきましょう。

各プールに持たせるクラスを定義する

31行目から36行目で、それぞれのプールに対して、持たせたい変数をまとめたクラスを定義していて、39行目でそのクラスをListで管理しています。

// 各プールで持たせたい変数をまとめたクラス
[System.Serializable]
public class PoolContent
{
    public PoolType type;
    public GameObject obj;
}

// インスペクターから複数のPoolContentを管理
[SerializeField] List<PoolContent> contents;

このクラスは簡単に言えば、それぞれのプール毎における処理を分割させるためのクラスとなっています。

ここではPoolContentというクラス名にしていて、この中で先ほど定義したPoolType型の変数と、管理したいオブジェクトの変数publicを付けて宣言しています。

そして、それぞれのプール毎のPoolContentをList型で管理するようにしています。

また、定義したクラスの頭の部分で[System.Serializable]、Listの頭の部分で[SerializeField]と記述しておくことで、インスペクターウィンドウからプールの内容を設定できるようになります。

上記のように、作りたいオブジェクトプールの数だけ+ボタンでリスト数を追加しておき、PoolTypeの値に合わせてPrefab化したオブジェクトを入れておきます。

Dictionary型で複数のオブジェクトプールを管理していく

41行目で、複数のオブジェクトプールをまとめるためのDictionary型を定義しています。

// PoolTypeをキーにしてオブジェクトプールにアクセスさせる
Dictionary<PoolType, ObjectPool<GameObject>> pools = new Dictionary<PoolType, ObjectPool<GameObject>>();

このDictionaryは、PoolType型をキーObjectPool型を値、としてペアで格納していくものになっています。

簡単に言えば、PoolTypeを指定することで、そのオブジェクトプールにアクセスすることができるようになります。

オブジェクトプールをDictionaryに登録していく

あとは、Startメソッド内の46行目から60行目で、それぞれのオブジェクトプールのインスタンスを生成して、それを先ほどのDictionary型の変数に登録していきます。

foreach (var content in contents)
{
    // それぞれのオブジェクトプールを作成する
    ObjectPool<GameObject> pool = new ObjectPool<GameObject>(
        () => Instantiate(content.obj),
        (obj) => OnGetObj(obj),
        (obj) => obj.SetActive(false),
        (obj) => Destroy(obj),
        true,
        2,
        20);

    // Dictionaryに登録する
    pools.Add(content.type, pool);
}

ここでは、foreach文でListの中で管理しているPoolContentを一つずつ取り出してきて、その内容でオブジェクトプールを作成しています。

そして、Addメソッドを使ってDictionaryへの登録を行っています。

これで、作成された複数のオブジェクトプールがPoolTypeをキーとして、1つのDictionaryに格納することができました。

事前にオブジェクトを生成しておく処理

今回は66行目から81行目で、事前にオブジェクトプール内にオブジェクトを入れておく処理を記述しています。

// 20個ずつオブジェクトを生成
void InitialStack()
{
    GameObject[] obj = new GameObject[20];

    foreach (var content in contents)
    {
        for (int i = 0; i < 20; i++)
        {
            obj[i] = Instantiate(content.obj);
        }
        for (int i = 0; i < 20; i++)
        {
            pools[content.type].Release(obj[i]);
        }
    }
}

それぞれのオブジェクト毎に、事前に20個保存されるように、Instantiateメソッドで生成する処理を行った後に、Releaseメソッドでプールに戻す処理を行っています。

なお、このInitialStackメソッドは、Startメソッド内の62行目で実行するようにしています。

オブジェクトプールからの取り出し・返却メソッド

あとは、外部のクラスからオブジェクトを取り出したり返却したりできるようにしたいので、91行目から100行目でそれぞれの処理を行うメソッドをpublicで定義しています。

// 外部のクラスからオブジェクトを取得するためのメソッド
public void OnGet(PoolType type)
{
    pools[type].Get();
}

// 外部のクラスからオブジェクトを返却するためのメソッド
public void OnRelease(PoolType type, GameObject obj)
{
    pools[type].Release(obj);
}

どちらのメソッドも引数でPoolTypeの値を受け取って、その値からDictionaryを使ってオブジェクトプールにアクセスして、取り出し(Getメソッド)返却(Releaseメソッド)の処理を行っています。

ここまでで複数のオブジェクトプールを管理していく仕組みを作ることができました。

オブジェクトを取り出す処理を実行するクラスを作る

あとは、オブジェクトプールから取り出す処理を実行させるクラスを作っていきます。

CoinGeneratorという空オブジェクトを作成して、以下のスクリプトをアタッチしておきます。

using UnityEngine;
using TMPro;

public class CoinGenerator : MonoBehaviour
{
    [SerializeField] TMP_InputField tmp;
    int inputValue;

    public void ButtonClick()
    {
        // 入力テキストが空では無い場合
        if (tmp.text != string.Empty)
        {
            inputValue = int.Parse(tmp.text);
        }

        // 値が0より大きい場合
        if (inputValue > 0)
        {
            for (int i = 0; i < inputValue / 100; i++)
            {
                PoolManager.instance.OnGet(PoolManager.PoolType.Coin100);   // 100円玉のオブジェクトをプールから取り出す
            }
            for (int i = 0; i < (inputValue % 100) / 10; i++)
            {
                PoolManager.instance.OnGet(PoolManager.PoolType.Coin10);    // 10円玉のオブジェクトをプールから取り出す
            }
            for (int i = 0; i < (inputValue % 100) % 10; i++)
            {
                PoolManager.instance.OnGet(PoolManager.PoolType.Coin1);     // 1円玉のオブジェクトをプールから取り出す
            }
        }
    }
}

今回は、ボタンをクリックした際にオブジェクトを取り出したいので、ButtonClickというメソッドを定義しています。

その中の14行目で、InputFieldのテキストが空欄でない場合に、取得したテキストをint型に変換して変数に代入しています。

なお、InputFieldのContent Typeの項目を「Integer Number」に設定しておくことで、int型の整数しか入力できないようになります。

そして、値に応じてそれぞれのコインをオブジェクトプールから取り出すために、「PoolManager.instance.OnGet」と記述してメソッドを呼び出して、引数にPoolTypeを指定しています。

あとは、このButtonClickのメソッドをButtonコンポーネントのOnClickイベントに登録しておきます。

最後に、コインのオブジェクトが画面外に行った際に、コインオブジェクトに紐づけたスクリプト(CoinController)で、オブジェクトをプールに戻す処理を記述しておきます。

using UnityEngine;

public class CoinController : MonoBehaviour
{
    Rigidbody2D rb;
    [SerializeField] PoolManager.PoolType poolType; // インスペクターからPoolTypeを指定する

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    // オブジェクトがゲーム画面に表示された場合
    void OnBecameVisible()
    {
        rb.AddForce(new Vector3(Random.Range(-0.5f, 0.5f), 1.0f, 0) * 500f);
    }

    // オブジェクトがゲーム画面から消えた場合
    void OnBecameInvisible()
    {
        PoolManager.instance.OnRelease(poolType, gameObject);   // オブジェクトをプールに返却する処理
    }
}

6行目でPoolType型の変数を宣言しておき、それぞれのPrefabを選択して、インスペクターウィンドウから値を指定しておきます。

そして、22行目でOnReleaseメソッドを呼び出して、引数でPoolTypeの変数を指定してあげれば完成です。

これでゲームを実行してEnterボタンを押してみると、

入力した数字に応じてオブジェクトが表示されるようになり、オブジェクトプールから再利用できているのも分かります。

まとめ

このページでは、Unityで複数のオブジェクトプールをより効率的に管理する方法についてまとめていきましたが、いかがでしたでしょうか?

複数のオブジェクトプールを作る場合は、全てのプールを一つのクラス内で管理することで、より効率的にオブジェクトプールを使うことができます。

ポイントとしては、Dictionaryのキーにenum型(オブジェクトの種類)を指定して、それとペアになる形でオブジェクトプールを紐づけてあげます。

これで管理することで、今後オブジェクトプールが増える場合であっても、簡単に追加していくことができると思います。

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

コメント