【Unity】段階的にObjectPoolに置き換えていく

Unityのよくある話として、Instantiate()Destroy()が重いという話は聞いた事があると思います。
シューティングゲームを作ると仮定して考えてみましょう。

まずは作る

まずは何もリソース管理を意識しないで作ってみます。

Bulletクラス

Bulletクラスを作成します。
Update()で移動し、一定秒数進んだらDestroy()を行うクラスです。

using System;
using UnityEngine;

/// <summary> 弾 </summary>
public class Bullet : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    private const float TotalTime = 1.0f;

    /// <summary>
    /// 時間
    /// </summary>
    private float _lifetimer = 0.0f;

    /// <summary>
    /// 移動スピード
    /// </summary>
    private float _speed = 5.0f;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        _lifetimer = TotalTime;
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        transform.position += (transform.right * (_speed * Time.deltaTime));

        _lifetimer -= Time.deltaTime;
        if (_lifetimer <= 0.0f)
        {
            Destroy(this.gameObject);
        }
    }
}

Gunクラス

弾を打つためのGunクラス。
Spaceを押すと弾を生成するクラスです。

/// <summary> 銃 </summary>
public class Gun : MonoBehaviour
{

    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    /// <summary>
    /// 弾
    /// </summary>
    [SerializeField]
    private Bullet _sourceBullet= null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        if (_sourceBullet== null)
        {
            Debug.Log($"{nameof(Bullet)} null!");
        }
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(_sourceBullet, transform.position, Quaternion.identity);
        }
    }
}

実行結果


弾の数が増えれば増えるほどInstantiateDestroyが走ります。
わざわざ生成と破壊処理を繰り返すのではなく、
アクティブ状態を切り替えた方が効率的です。

アクティブ状態を切り替えるようにする

それではアクティブ状態を切り替えられるように
クラスを改良していきます。

Bulletクラス

まずはBulletクラスから。
値の初期化をStart()で行っていましたが、
Init()という公開関数を用意して個別で初期化できるようにします。

また、_lifeTimerが0以下になった時にDestroy()を行うのではなく、
自身の役割の終了を伝えるデリゲート
Action<Bullet> _timerFinishCallBackを用意しておきます。

using System;
using UnityEngine;

/// <summary> 弾 </summary>
public class Bullet : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    private const float TotalTime = 1.0f;

    /// <summary>
    /// 時間
    /// </summary>
    private float _lifetimer = 0.0f;

    /// <summary>
    /// 移動スピード
    /// </summary>
    private float _speed = 5.0f;

    /// <summary>
    /// 時間が0になった時に呼ぶデリゲート
    /// </summary>
    private Action<Bullet> _timerFinishCallBack = null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        transform.position += (transform.right * (_speed * Time.deltaTime));

        _lifetimer -= Time.deltaTime;
        if (_lifetimer <= 0.0f)
        {
            _timerFinishCallBack.Invoke(this);
        }
    }

    /// <summary>
    /// 破壊時処理
    /// </summary>
    private void OnDestroy()
    {
        ClearTimerFinishCallBack();
    }

    //-----------------------------------------------------------------------
    // 公開関数
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化
    /// </summary>
    public void Init(Vector3 pos)
    {
        transform.position = pos;
        transform.rotation = Quaternion.identity;
        _lifetimer = TotalTime;
    }

    /// <summary>
    /// デリゲート登録
    /// </summary>
    /// <param name="func">時間が0になった時に呼ぶデリゲート</param>
    public void RegisterTimerFinishCallBack(Action<Bullet> onAction)
    {
        _timerFinishCallBack = onAction;
    }

    /// <summary>
    /// デリゲート解除
    /// </summary>
    public void ClearTimerFinishCallBack()
    {
        _timerFinishCallBack = null;
    }
}

Gunクラス

Gunクラス側ではQueue<Bullet> _bulletsを用意。
弾のアクティブ/非アクティブ処理はGun側で制御します。

弾を打つ時に_bulletsの数を確認し、存在していれば取り出して使用します。

もし数が足りない場合は新しく生成し、
RegisterTimerFinishCallBack_bulletsに格納し直すアクション
である
OnFinishActionを登録しておきます。

using System.Collections.Generic;
using UnityEngine;

/// <summary> 銃 </summary>
public class Gun : MonoBehaviour
{

    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    /// <summary>
    /// 弾
    /// </summary>
    [SerializeField]
    private Bullet _sourceBullet = null;

    /// <summary>
    /// 弾を格納するキュー
    /// </summary>
    private Queue<Bullet> _bullets = null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        _bullets = new();

        if (_sourceBullet == null)
        {
            Debug.Log($"{nameof(Bullet)} null!");
        }
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if(_bullets.Count > 0)
            {
                // 既にある物から取り出して初期化
                var bullet = _bullets.Dequeue();
                bullet.Init(transform.position);
                bullet.gameObject.SetActive(true);
            }
            else
            {
                // 新たに生成
                var bullet = Instantiate(_sourceBullet);

                // デリゲード登録->初期化
                bullet.RegisterTimerFinishCallBack(OnFinishAction) 
                bullet.Init(transform.position);
            }
        }
    }

    /// <summary>
    /// プールに格納する処理
    /// </summary>
    /// <param name="bullet">弾</param>
    private void OnFinishAction(Bullet bullet)
    {
        _bullets.Enqueue(bullet);
         bullet.gameObject.SetActive(false);
    }
}

実行結果


自前で処理が完了しました。
時間が0以下になった時に非アクティブになっています。

ObjectPoolをつかう

自前で作成したものをObjectPoolに置き換えていきます。
docs.unity3d.com

ObjectPoolのコンストラクタ引数は以下の通りです。
Unity - Scripting API: Pool.ObjectPool_1.ObjectPool<T0>

引数名 説明 主な処理内容・備考
Func <T> createFunc プールが空の時に呼ばれるデリゲート Instantiate()->初期化処理 等
Action<T> actionOnGet プールからの取り出し時に
呼ばれるデリゲート
SetActive(true)->初期化処理 等
Action<T> actionOnRelease プール格納時に呼ばれるデリゲート SetActive(false) 等
Action<T> actionOnDestroy プールに格納しようとしたが
maxSizeを超えていた時
Destroy() 等
bool collectionCheck 同一インスタンスを使用した際に
例外を投げるか
例外処理はEditorのみ
int defaultCapacity デフォルトの容量 -
int maxSize 最大サイズ 最大サイズを超えると
actionOnDestroyが呼ばれる

※処理内容はあくまで個人的な仕様用途で

取得はGet()。使わなくなったらRelease()を呼びます。
Unity - Scripting API: Pool.ObjectPool_1.Release
Unity - Scripting API: Pool.ObjectPool_1.Get

Gunクラス

_bulletsObjectPoolに置き換えて
Update()の弾生成時に行っていたことを
Start()内、_bulletsの初期化処理に置き換えていきます。

using UnityEngine;
using UnityEngine.Pool;

/// <summary> 銃 </summary>
public class Gun : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    private const int Capacity = 3;

    private const int MaxSize = 5;

    /// <summary>
    /// 弾
    /// </summary>
    [SerializeField]
    private Bullet _sourceBullet = null;

    /// <summary>
    /// オブジェクトプール
    /// </summary>
    private ObjectPool<Bullet> _bullets = null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        if (_sourceBullet == null)
        {
            Debug.Log($"{nameof(Bullet)} null!");
        }

        // プールの生成
        _bullets = new ObjectPool<Bullet>(
            // インスタンス生成時
            () =>
            {
                // 生成
                var obj = Instantiate(_sourceBullet);
                var bullet = obj.GetComponent<Bullet>();

                // Poolに格納するデリゲートの登録->初期化
                bullet.RegisterTimerFinishCallBack(OnFinishAction);
                bullet.Init(transform.position);
                bullet.gameObject.SetActive(true);
                return bullet;
            },

            // プール取り出し時
            (b) =>
            {
                b.Init(transform.position);
                b.gameObject.SetActive(true);
            },

            // プール格納時
            (b) => b.gameObject.SetActive(false),

            // プールに戻せなかった時
            (b) => Destroy(b.gameObject),

            // 同一インスタンス時例外を投げるか?
            true,

            // スタックのデフォルト容量
            Capacity,

            // 最大サイズ
            MaxSize);

    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 取得はGet
            _bullets.Get();
        }
    }

    /// <summary>
    /// プールに格納する処理
    /// </summary>
    /// <param name="bullet">弾</param>
    private void OnFinishAction(Bullet bullet)
    {
        // 格納はRelese
        _bullets.Release(bullet);
    }
}

実行結果


非アクティブ化の処理に加えて、
最大数を超えたときにDestroy()してくれるようになりました。
これで効率よく弾が発射できるようになりました。

参考サイト

Unity - Scripting API: ObjectPool<T0>