【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); } } }
実行結果
弾の数が増えれば増えるほどInstantiate
とDestroy
が走ります。
わざわざ生成と破壊処理を繰り返すのではなく、
アクティブ状態を切り替えた方が効率的です。
アクティブ状態を切り替えるようにする
それではアクティブ状態を切り替えられるように
クラスを改良していきます。
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クラス
_bullets
をObjectPoolに置き換えて、
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()
してくれるようになりました。
これで効率よく弾が発射できるようになりました。