なんかよく聞く「ObjectPool」について知ってもらおう!という記事です。
大量のゲームオブジェクトを扱いたい!
Unityでは当然ながら、ゲームオブジェクト(とそれに紐づくクラス)を扱います。
(例)CYGNI : ALL GUNS BLAZING
https://store.steampowered.com/app/1248080/CYGNI_All_Guns_Blazing/?l=japanese
例えば、上記画像のシューティングゲームを例に出して考えてみましょう。
- 自分の機体
- 味方の機体 × 複数
- 敵の機体 ×複数
- 自分の発射した弾 ×沢山
- 敵の発射した弾 ×沢山
- ステージ
に加え、付随するエフェクトも多岐に渡ります。
当然、その分のゲームオブジェクトを生成し、管理する必要があります。
Instantiate / Destroyは重い(らしい)
ここで問題になってくるのが、InstantiateとDestroyです。
InstantiateとDestroyは結構重い処理(らしい)!
(重いという話は聞くよね...)
とにかく生成→破棄を多用するのは避けたいところ!!
では、どのようにこの問題を解決すれば良いのでしょうか。
ObjectPool パターン
オブジェクトプールパターンでは、非アクティブ化された「プール」に使用準備が整った状態で待機している一連の初期化されたオブジェクトを使用します。
オブジェクトが必要なときに、アプリケーションではそれをインスタンス化しません。代わりに、プールからゲームオブジェクトを要求して、それを有効にします。
使用が終わった際には、そのオブジェクトを破壊する代わりに、非アクティブ化してプールに戻します。
参考(ゲームプログラミングパターンでプログラミングをレベルアップ Unity公式)
👦「なんのこっちゃ」
端的に言えば、
使いまわせる「在庫」を用意しよう!
使う時は在庫から出して、使い終わったら戻してね!
ということです。
ObjectPoolパターンに則れば、基本的にDestroyを使いません。
また、Instantiateを使用するのは在庫の生成(および補充)の時だけで済みます。
以下に、動画を掲載しました。
弾を発射する際はアクティブ化して使用し、終わったら自動的に非アクティブに!
また、在庫が足りなくなった時に追加されているのも分かります。
具体的なコード
ここからは、ObjectPoolを具体的なコードと共に見ていきましょう。
- Unity公式が提供しているデザインパターンの教材である
ゲームプログラミングパターンでプログラミングをレベルアップおよび
より多くのデザインパターンとSOLIDの原則に記載のコード
- 少し手を加えた応用コード
の二本立てで紹介します。
公式コード
まずは、公式コードでイメージを掴みましょう。
下記のページに公式のObjectPool解説用コードの全文を記載します。
ObjectPoolパターン 公式コード
・PooledObjectクラス(在庫側)
public class PooledObject : MonoBehaviour
{
private ObjectPool pool;
public ObjectPool Pool { get => pool; set => pool = value; }
public void Release()
{
pool.ReturnToPool(this);
}
}
・ObjectPoolクラス(在庫管理側)
public class ObjectPool : MonoBehaviour
{
[SerializeField] private uint initPoolSize;
[SerializeField] private PooledObject objectToPool;
// コレクション内のプールされたオブジェクトを格納する
private Stack<PooledObject> stack;
private void Start()
{
SetupPool();
}
// プールを作成する(ラグが目立たないときに呼び出す)
private void SetupPool()
{
stack = new Stack<PooledObject>();
PooledObject instance = null;
for (int i = 0; i < initPoolSize; i++)
{
instance = Instantiate(objectToPool);
instance.Pool = this;
instance.gameObject.SetActive(false);
stack.Push(instance);
}
}
public PooledObject GetPooledObject()
{
// プールの大きさが十分でない場合は、新しい PooledObjects をインスタンス化する
if (stack.Count == 0)
{
PooledObject newInstance = Instantiate(objectToPool);
newInstance.Pool = this;
return newInstance;
}
// それ以外の場合は、リストから次のものをグラブする
PooledObject nextInstance = stack.Pop();
nextInstance.gameObject.SetActive(true);
return nextInstance;
}
public void ReturnToPool(PooledObject pooledObject)
{
stack.Push(pooledObject);
pooledObject.gameObject.SetActive(false);
}
}
その上で、公式コードをひとつずつ解説していきます!
👦「頑張りましょう!」
公式コードには在庫管理をする側であるObjectPoolクラスと、管理される側のPooledObjectクラスがあり、
- フィールド
- 在庫を生成する void SetupPool()
- 在庫から取り出して使用する PooledObject GetPooledObject()
- 使い終わったら戻す void ReturnToPool (PooledObject pooledObject)
という部分に分かれています。
1. フィールド
フィールドで定義されているのは以下です。
[SerializeField] private uint initPoolSize;
[SerializeField] private PooledObject objectToPool;
// コレクション内のプールされたオブジェクトを格納する
private Stack<PooledObject> stack;
[SerializeField](privateだがInspectorから設定可能)
-
initPoolSize:在庫の数を決める。(uintは負の値を取らないint)
-
objectToPool:PooledObjectのインスタンス。これを元に在庫を生成する。
-
stack:生成したPooledObjectを格納する。
(StackはListと似た機能をもつデータ構造。先入れ後出しの性質を持つ。)
2. 在庫を生成する
以下が、在庫を生成する部分です。
private void Start()
{
SetupPool();
}
// プールを作成する(ラグが目立たないときに呼び出す)
private void SetupPool()
{
stack = new Stack<PooledObject>();
PooledObject instance = null;
for (int i = 0; i < initPoolSize; i++)
{
instance = Instantiate(objectToPool);
//ObjectPoolのインスタンスを渡している
instance.Pool = this;
instance.gameObject.SetActive(false);
stack.Push(instance);
}
}
SetupPool()内の動き
- InstantiateによってPooledObjectを生成します。
- 生成したPooledObjectに対して、このObjectPoolのインスタンスを渡しています。
(在庫にPooledObjectを戻す際に、PooledObject側からObjectPoolを参照するためです。)
- 生成したPooledObjectを非アクティブにします。
(未使用状態のときは、非アクティブにします。)
- stackに格納します。
- 1~4をinitPoolSize分繰り返します。
これで、在庫の入ったstackができました!
3. 在庫から取り出して使用する
以下が、取り出す部分です。
public PooledObject GetPooledObject()
{
// プールの大きさが十分でない場合は、新しい PooledObjects をインスタンス化する
if (stack.Count == 0)
{
PooledObject newInstance = Instantiate(objectToPool);
newInstance.Pool = this;
return newInstance;
}
// それ以外の場合は、リストから次のものをグラブする
PooledObject nextInstance = stack.Pop();
nextInstance.gameObject.SetActive(true);
return nextInstance;
}
GetPooledObject()内の動き
- stack.Pop()により、stack(在庫)から使用するPooledObjectを一つ取り出します。
- 取り出したオブジェクトをアクティブにします。
- 全ての在庫が使用されており出払っている(stackの数が0)の場合のみ、新しいインスタンスを生成します。
これで、stackから取り出すことができるようになりました!
4. 使い終わったら戻す
以下が、在庫に戻す部分です。
public void ReturnToPool(PooledObject pooledObject)
{
stack.Push(pooledObject);
pooledObject.gameObject.SetActive(false);
}
public class PooledObject : MonoBehaviour
{
private ObjectPool pool;
public ObjectPool Pool { get => pool; set => pool = value; }
public void Release()
{
pool.ReturnToPool(this);
}
}
在庫に戻す際は、使用されるPooledObject側が、自分でstackに戻す処理を実行します。
ReturnToPool内の動き
- 受け取ったpooledObjectを、stack.Push()によりstackに戻します。
- 当該オブジェクトを非アクティブにします。
PooledObjectクラス内の動き
- プロパティでObjectPoolクラスのインスタンスをpool変数に保存します。
- PooledObjectは、自身の使用を終わる際にRelease()内でObjectPoolクラスを参照し、ReturnToPoolメソッドに自身を渡すことで在庫に戻ります。
これで、ObjectPoolの機能は全てできるようになりました!
応用コード
次は、応用コードを見てみましょう。
👦「まだあるのか...」
下記のページに改変したObjectPool用コードの全文を記載します。
ジェネリックと抽象を使用した、汎用的なものとしています。
ObjectPoolパターン 応用コード
・APooledObjectクラス(在庫側の抽象クラス)
using System;
using UnityEngine;
public abstract class APooledObject : MonoBehaviour
{
private Action<APooledObject> poolAction;
public void Release()
{
if(poolAction == null)return;
poolAction?.Invoke(this);
}
public void SetPoolAction(Action<APooledObject> action)
{
poolAction += action;
}
}
・IObjectPool(在庫管理側インターフェース)
using UnityEngine;
public interface IObjectPool
{
void PoolSetUp(uint index);
APooledObject GetFromPool();
void ReturnToPool(APooledObject pooledObject);
}
・ObjectPoolクラス(在庫管理側ジェネリッククラス)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class ObjectPool<T> : IObjectPool where T : APooledObject
{
private Stack<APooledObject> _pool;
private IFactory _factory;
public ObjectPool(IFactory factory)
{
_pool = new Stack<APooledObject>();
_factory = factory;
}
public void PoolSetUp(uint initPoolSize)
{
//Stackの初期化
if (_factory == null)
{
return;
}
Stack<APooledObject> objectPool = _pool;
//とりあえずPoolSize分instanceを生成して、見えなくしておく
for (int i = 0; i < initPoolSize; i++)
{
T instance = ObjectInstantiate();
if(instance == null)return;
instance.gameObject.SetActive(false);
objectPool.Push(instance);
}
}
public APooledObject GetFromPool()
{
if(_factory == null)
{
return null;
}
Stack<APooledObject> objectPool = _pool;
// プールに在庫があればそれを使用、なければ新規作成
if (objectPool.Count < 1)
{
T newInstance = ObjectInstantiate();
if(newInstance == null)return null;
return newInstance;
}
T nextInstance = objectPool.Pop() as T;
nextInstance.gameObject.SetActive(true);
return nextInstance;
}
public T ObjectInstantiate()
{
T instance = _factory.ObjectInstantiate() as T;
if(instance == null)return null;
instance.SetPoolAction(ReturnToPool);
return instance;
}
// 弾をプールに戻す
public void ReturnToPool(APooledObject pooledObject)
{
Stack<APooledObject> objectPool = _pool;
objectPool.Push(pooledObject);
pooledObject.gameObject.SetActive(false);
}
}
また、汎用性を上げるためにFactoryパターンを併用しています。
流れは公式と同じですが、以下変わった部分を説明していきます
1.抽象クラスを使用
2.インターフェースの追加
3.より使いやすいクラス
(在庫の生成にFactoryパターンを用いることができる)
まずは、管理される側であるPooledObjectクラスを見ていきます。
1. 抽象クラスを使用
・在庫に必要な機能を持たせた抽象クラス「APooledObject」
using System;
using UnityEngine;
public abstract class APooledObject : MonoBehaviour
{
private Action<APooledObject> poolAction;
public void Release()
{
poolAction?.Invoke(this);
}
public void SetPoolAction(Action<APooledObject> action)
{
poolAction += action;
}
}
ポイント
・オブジェクトを解放した時に実行するメソッドを複数登録できます。
2. インターフェースの追加
・ObjectPoolであることを表すインターフェースを追加
using UnityEngine;
public interface IObjectPool
{
void PoolSetUp(uint index);
APooledObject GetFromPool();
void ReturnToPool(APooledObject pooledObject);
}
ポイント
・複数のObjectPoolクラスを作成する時に便利です。
・IObjectPoolを実装したクラスは、在庫生成を行うPoolSetUpメソッドを持ちます。
・IObjectPoolを実装したクラスは、管理している在庫を取り出すGetFromPoolメソッドを持ちます。
・IObjectPoolを継承したクラスは、使用後に返却するReturnToPoolメソッドを持ちます。
3. より使いやすいクラス
ジェネリック
public class ObjectPool<T> : IObjectPool where T : APooledObject
ポイント
・ObjectPoolの対象をわかりやすくしています。
Factoryを導入したオブジェクト作成
private Stack<APooledObject> _pool;
private IFactory _factory;
public ObjectPool(Transform poolTransform, IFactory factory)
{
_pool = new Stack<APooledObject>();
_transform = poolTransform;
_factory = factory;
}
public T ObjectInstantiate()
{
T instance = _factory.ObjectInstantiate() as T;
if(instance == null)return null;
instance.SetPoolAction(ReturnToPool);
return instance;
}
ポイント
オブジェクト生成にFactoryパターンを用いており、IFactoryというインターフェースがある前提とします。
(ゲームプログラミングパターンでプログラミングをレベルアップの39ページ〜と同じです)
・Factoryを元にオブジェクトを生成することで、生成過程をObjectPoolと切り離せます。より複雑なオブジェクトにもObjectPoolを使用できます。
参考サイト