i-school - 【2D】オブジェクトプールを活用した敵の生成機能の実装例
 2Dゲームにおいて、敵の生成機能に関しての発展例です。



 オブジェクトプールの機能を使い、同じ敵をリサイクルして繰り返し利用する方法に置き換えます。

 オブジェクトプールは、再利用可能なオブジェクトのセットを管理する仕組みです。
アプリケーション内で頻繁に生成と破棄を行うオブジェクトをプール(非表示化)しておき、
生成と破棄の代わりに再利用(再表示)することでパフォーマンスを向上させることができます。


<実装動画 ヒエラルキーに倒した敵のゲームオブジェクトが非表示状態で残っており、再表示されて使われている>
動画ファイルへのリンク



1.設計


 リファクタリングの対象は敵を生成する際の処理になります。

 いままで敵を破壊して再度生成いましたが、この部分をオブジェクトプールで管理します。
この手法を用いた場合、敵は破壊されるのではなく、非表示の状態になり、プールされます。


◇ 1.敵をオブジェクトプールから取り出して画面に表示する
    オブジェクトプール内にない場合のみ、新しく生成する

◇ 2.敵を破壊する場合、代わりにオブジェクトプールにすべて戻す

 このような処理の流れになります。
◇のついている部分が新しく採用するオブジェクトプールの機能です。


2.EnemyController クラスを作成して、オブジェクトプールに対応するための機能を追加する


 EnemyController クラスを作成し、オブジェクトプールに対応するための機能を追加します。

 using UnityEngine.Pool の宣言が必要になります。



EnemyController.cs

<= クリックしたら開きます。




 処理のポイントはプロパティの活用方法です。
オブジェクトプールを参照先としておくことにより、このクラスからいつでもオブジェクトプール
(今回は次に修正する GeneratorBase クラス)へのアクセスを行えるようにしています。

 そのため、新しく追加した ReleaseBullet メソッドを任意のタイミングで実行できる状態を作り、
敵側からオブジェクトプールに戻す処理を実装出来ます。


3.EnemyGeneratorObjectPool クラスを作成してオブジェクトプールの機能を追加する


 EnemyGeneratorObjectPoolクラスを作成し、オブジェクトプールの機能を追加します。

 using UnityEngine.Pool の宣言が必要になります。



EnemyGeneratorObjectPool.cs

<= クリックすると開きます



4.オブジェクトプール


 オブジェクトプールとは、デザインパターンの1つです。

 その概念をゲーム内でも利用できるように Unity の用意している機能の1つでもあります。
以前は自作して利用していましたが、Unity 2021 より、Unity の標準機能として実装されました。


参考サイト
Unity 公式マニュアル
オブジェクトプール
【Unity】オブジェクトプール(Object Pool)の使い方!Unity2021から標準で使えるぞ
Zenn twugo様
(非公式和訳)Unity - level up your code with game programming patterns Chapter 05 オブジェクトプール(Object Pool)
【Unity】Unity公式のObjectPoolを使ってみる(内部実装も一部紹介)


1.オブジェクトプールの初期化


 コンストラクタメソッドを定義します。
この部分における初期化とは、オブジェクトプールの機能を設定する、という意味合いになっています。
そのため、この部分でオブジェクトを生成している訳ではなく、どのような挙動をするのかを決める、という内容の初期化処理です。


参考サイト
Unity 公式ドキュメント
ObjectPool T0 Constructor



    // オブジェクトプールの初期設定。new だが、インスタンスを作る訳ではない
        enemyPool = new ObjectPool<EnemyController>(
            createFunc: () => Create(),
            actionOnGet: OnGetFromPool,  // メソッド作成して登録できる
            actionOnRelease: target => target.gameObject.SetActive(false),
            actionOnDestroy: target => Destroy(target.gameObject),
            collectionCheck: true,
            defaultCapacity: 10,
            maxSize: 1000);

 ObjectPool はジェネリック<T>ですので、利用するときに型を指定します。
ここでは EnemyController 型にしています。抽象化しておくことにより、特定の敵ではなく、敵であればどれでも、という利用方法が出来ます。

 引数は7つあり、それぞれに指定された値を設定していくことで、オブジェクトプールの挙動を作ります。

 この例では各引数について名前付き引数の機能を利用して書き込んでいますが、値だけを記述しても問題ありません。


参考サイト
MicroSoft
名前付き引数と省略可能な引数 (C# プログラミング ガイド)


2.第1〜4引数


 第1〜4引数までは、特定のイベント発生時の実行処理を設定します。OnTriggerEnter メソッドなどのコールバック処理のイメージです。
「こういう状態になったら(特定のイベント発生したら)」⇒「設定した(紐付けした)処理を実行する」というパターンです。
このうち、「設定した(紐付けした)処理を実行する」の部分はデリゲートになっており、ラムダ式による匿名メソッドが利用できます。



 第1引数は createFunc です。これは、IObjectPool.Get() メソッドにより実行されます。
この Get メソッドは内部で自動分岐し、オブジェクトプール内にオブジェクトがある場合には、次の第2引数に登録されているメソッドが実行されます。
そしてオブジェクトプール内にオブジェクトがない場合に、この第1引数の処理が実行されます。

 これは Func<T> 型です。ここにはオブジェクトプールに対して生成の命令がきた場合の挙動を設定します。
主にプールが空のときに新しいオブジェクトのインスタンスを生成する機能を設定します。
() => Create() を指定していますので、生成命令がきた場合には Create メソッドを実行します。

 第2引数以降も同様です。特定のイベントに紐づける形でデリゲートを設定していきます。



 第2引数は actionOnGet です。これは、IObjectPool.Get() メソッドにより実行されます。
 こちらは Action<T> 型です(戻り値がありません)。
ここにはオブジェクトプール内にプールされて非表示の状態になっているオブジェクトを取り出す際の処理を設定します。
OnGetFromPool メソッドを指定していますので、オブジェクトの取り出し命令が来たら、OnGetFromPool メソッドが実行されます。



 第3引数は actionOnRelease です。これは IObjectPool.Release() メソッドにより実行されます。
 こちらも Action<T> 型です。
このメソッドはオブジェクトプールと同じ型を第1引数に取ります。今回であれば EnemyController 型の情報を指定します。

 ここにはオブジェクトをオブジェクトプール内に戻す命令が来たときの処理を設定します。
target => target.gameObject.SetActive(false) と設定されていますので、引数で受けた target (今回は EnemyController 型)のオブジェクトを SetActive メソッドを利用して非表示します。
これにより、オブジェクトが再度、オブジェクトプール内に戻り、プールされます。

 このようにデリゲート部分には、メソッドだけではなく、処理が1行である場合にはラムダ式を利用して、直接処理を記述することも出来ます。



 第4引数は actionOnDestroy です。こちらも Action<T> 型です。
これには直接の実行命令はなく、オブジェクトプールが第7引数で設定する maxSizeに達した際(オブジェクトをプールに戻せなかったとき)に自動的に呼び出される処理を設定します。
プールからあふれてしまったオブジェクトに対しての処理になりますので、主にオブジェクトの破壊処理を設定します。
target => Destroy(target.gameObject) と設定されていますので、引数で受けた target のオブジェクトを破壊します。
この場合、このオブジェクトはプールされずに破棄されます。

 いずれの場合も、オブジェクトプールからの命令により、いずれかのイベントが実行されます。


3.第5〜7引数


 第5〜7引数は、オブジェクトプールの設定値です。



 第5引数は collectionCheck です。bool 型です。チェック機能のオンオフ切り替えです。ここでは true に設定しています。
true に設定しておくことで、オブジェクトのインスタンスがプールに戻されるときに自動的に実行されます。
オブジェクトのインスタンスをプールに戻す際、同一のインスタンスが登録されているか調べ、すでに登録がある場合は、例外がスローされます。
この機能はエディターでのみ実行されます。



 第6引数は defaultCapacity です。int 型です。ここでは 10 に指定しています。
プールで利用するコレクションの初期許容量を示しています。



 第7引数は maxSize です。int 型です。ここでは 1000 に指定しています。
オブジェクトプールの最大サイズです。この値がオブジェクトプール内に保持できるオブジェクトの総数になります。
オブジェクトプールが最大サイズに達すると、プールに返されたそれ以上のインスタンスは無視され、ガベージコレクションされる可能性があります。
そのため、この値を適切に使用すると、プールのサイズが非常に大きくなるのを防ぐことができます。



5.オブジェクトプール専用のプレハブを作成する


 すでに敵用のプレハブがある場合には、そちらを利用しましょう。

 オブジェクトプールでは EnemyController 型の情報で敵を管理しますので、
EnemyController スクリプトがアタッチされていれば、現在あるプレハブもそのまま流用できます。


<Prefabs フォルダ>






6.EnemyGenerator ゲームオブジェクトを作成し、EnemyGeneratorObjectPool スクリプトをアタッチして設定を行う


 ヒエラルキーにて Create Empty を行い、新しいゲームオブジェクトを作成し、EnemyGenerator に名前を変更します。
こちらに EnemyGeneratorObjectPool スクリプトをアタッチします。

 アタッチされているゲームオブジェクトのインスペクターを確認し、新しく追加した変数の設定を行います。

 すでに EnemyGenerator ゲームオブジェクトが存在している場合には、それを複製して非表示にしておきます。
新しく作成した EnemyGeneratorObjectPool スクリプトを複製した EnemyGenerator ゲームオブジェクトにアタッチして、
元々ある EnemyGenerator スクリプトの情報を移してから EnemyGenerator スクリプトを Remove しましょう。

 そうすることで、元々の EnemyGenerator の設定を使って EnemyGeneratorObjectPool スクリプトを運用出来ます。



 enemyPrefab 変数には、生成したい敵のプレハブをアサインします。
EnemyController スクリプトがアタッチされているプレハブであれば、どれでもアサイン出来ます。

 generateInterval 変数は生成するまでの待機時間、maxGenerateCount 変数は生成する敵の総数です。

 下記は参考値です。


インスペクター画像



ゲームを実行して動作を確認する


 ゲームを実行して動作を確認しましょう。

 漠然と動かすのではなく、自分が作成したスクリプトの内容を確認し、処理が正常に動作しているかを見極める力を是非養っていってください。

 ヒエラルキーや、インスペクターの値を確認しながらデバッグしていく必要があります


<実装動画 ヒエラルキーに倒した敵のゲームオブジェクトが非表示状態で残っており、再表示されて使われている>
動画ファイルへのリンク


 上手く動かない場合には、Unity 内の設定や、スクリプトの内容を見直してみてください。
 


 以上で完成です。

 正常に動作したら、EnemyGenerator を増やしたり、異なる敵を生成したりしてみましょう。