オブジェクト・プールというのはプログラミング・パターンのひとつで、あらかじめオブジェクトを生成してプール(備蓄)しておくことで実行中の負荷を減らそうという手法です。
Unityでもよく使われるパターンのひとつですが、Unity 2021にてオブジェクト・プールAPIが追加され、それを使えばゼロから全部自分でコードを書く必要がなくなりました。今回はそのAPIの使い方についてです。
※2024/03/15 間違いや不十分な部分があったため、記事全体を見直し必要な部分は加筆修正しました。
今回の記事はUnity公式が発行した、プログラミング・パターンについての解説本である"Level Up Your Code With Programming Patterns"を参考にしています。
UnityEngine.Pool
使い始めるための準備は、using 部に UnityEngine.Poolと書き込んで、プール対象となるタイプのIObjectPool<T>を宣言するだけです。
このプールを初期化するためにはいくつかのメソッドが必要になりますが、今回はプールでオブジェクトを生成する際に実行される、Createメソッドを作成します。
ここで注意が必要なのがInstantiateメソッドで生成するタイプについてです。今回はBeamControllerをプールするので、プレハブ用の変数をBeamControllerとして_beamPrefabを宣言しています。
プール対象のオブジェクトが自ら自身をプールに戻す場合、プールへの参照が必要になることからも、そのオブジェクトをコントロールしているスクリプトをタイプとして指定するのが多くの場合好ましいはずです。
ObjectPoolの初期化には、前述のCreateメソッドの他にオブジェクトをGet, Release, Destroyする時に実行されるメソッドも必要ですが、基本的にはSetActiveかDestroyを行うだけなので、ラムダ式で省略してしまっています。
(BeamController beamController) => { }
という風に空のラムダ式を書くことも可能です。もちろんより多くの処理を行いたい場合は、それぞれきちんとメソッドを書く必要が出てくると思います。
シューティングゲームの弾
というわけで実例に移りますが、シューティングの弾をプーリングしたいと思います。シューティングゲームにおける弾オブジェクトは大量に必要です。リアルタイムに生成、破壊を繰り返すのはものすごい負担になるのでオブジェクト・プールを使うのに最適かつ代表的なケースになります。
今回はSkyFighterBeamShooterとBeamControllerという二つのクラスが登場します。
弾自体をコントロールするためのクラスとして"BeamController"スクリプトを書きました。(ビームのゲームオブジェクト及び、そのプレハブに装着されます)
このクラスにIObjectPool<PlayerBeamController>のプロパティを持たせます。ここでは自動実装を使っていますが、本来なら private フィールドを宣言した方が良いでしょう。
SkyFighterBeamShooterクラスでは前述の通り、CreateBeamメソッド内でプレハブから弾をInstantiateして、そのIObjectPoolプロパティにSkyFighterBeamShooterで作成したプールを代入します。
これで各ビームが自分達を蓄えるプールへの参照を持ち、これを利用して、Releaseメソッドでビームのオブジェクトをプールに戻します。
では、Awake内でIObjectPoolを初期化します。前述の通り、その他の必要なメソッドはラムダ式として入力しています。
キャパシティ、最大値はそれぞれプール内オブジェクトの基本的な数とそれを超えるとしても最大何個までオブジェクトを生成するかを設定するものです。最大値を超えるとオブジェクトは破壊されます。
ここで注意なのですが、このままだとプールにオブジェクトは無い状態なので、結局実行時に生成されることになってしまいます。公式ドキュメントなどでは行われていないのですが、あらかじめ生成してプールしておいた方が良いです。(このままだとゲーム開始時に一定数が生成されることになります)
なので、for文を使って必要な数だけビーム・オブジェクトを生成します。処理としてはCreateBeam内とほぼ変わらず、オブジェクト生成してBeamController内のIObjectPoolプロパティに当該のプールを設定し、Releaseしてオブジェクトをプールに収めるだけです。
これでシーンがロードされる前に、弾をあらかじめ生成できました。ゲーム実行時からプール内にオブジェクトが十分にあるので負荷が減ります。プレイヤーを実行するとあらかじめプールされたオブジェクト群がヒエラルキー上にずらりと並びます。
GetとRelease
実際にコード内でプールからオブジェクトを取得するためにはGetメソッドを使います。これによりInstantiateメソッドを取り除き、余計なGC(ガベージコレクション)の発生を防ぐことが出来るようになりました。
上画像のメソッド内では弾の発射位置だけ入力しています。
ビーム弾のコントロールとして、敵などに衝突した後の処理の中でReleaseを使います。オブジェクト・プールを使わない場合、ここでDestroyを行うので大きな負担になっていました。Releaseするとプールに設定していたonRelease実行によりオブジェクトは無効化された後でプールに戻され、またGetメソッドで呼び出されるまで待機することになります。
まとめ
慣れてしまえば、かなり簡単にオブジェクトプール・パターンを使えるようになる便利なAPIだと思います。
上述したように、プール作成後にAwakeかStartで必要な分だけオブジェクトを生成してプールを満たしておくのが良いはずです。
UnityにはIncremental GC機能が追加され、GCによるパフォーマンス低下が抑えられるようになりましたが、GCを発生させないことこそが最大の最適化と言われますし、これからこの機能はどんどん使っていきたいと思います。