今回はUnityの最適化に関する話題で、Unity2021から標準で使える
をご紹介するという内容です。
Unityでシューティングゲーム等を作っていると弾を頻繁に生成・削除することになると思うのですが、頻繁にInstantiate→Destroyを繰り返すとゲームが重くなってしまうという問題があります。
そこでよく使われるテクニックが今回ご紹介するオブジェクトプールという手法です。ここでは
- オブジェクトプールとは?
- どれだけパフォーマンスが良くなるのか?
- Unityでオブジェクトプールを簡単に実装する方法
といった点について丁寧に解説していきますね。
オブジェクトプール(Object Pool)とは?
まずはじめに、「オブジェクトプールって何だよ」と思う方もいらっしゃるかもしれませんので簡単に概要を説明しておきます。
オブジェクトプールはゲーム開発における最適化テクニックの一つで、ゲームに使うオブジェクトの生成・削除の処理を最小限に抑えるための手法です。仕組みは割と単純で次のような感じになっています。
- 予めゲームオブジェクトを登録するディクショナリ等を作っておく。
- ゲームオブジェクトが生成されるとき、手順1.のディクショナリに同じオブジェクトがあればそれを再利用し、そうでなければ新しく生成してディクショナリに登録する。
- ゲームオブジェクトが削除されるとき、本当に削除するのではなくとりあえず非アクティブ化しておく。
オブジェクトの生成・削除処理は重いので、それをなるべく抑えることで処理を高速化しようという発想ですね。
ちなみにオブジェクトプールは色々なゲームで使われますが、特に弾幕シューティングゲームなど大量の弾を生成・削除するゲームで活用されています。
オブジェクトプールでどれだけパフォーマンスが良くなるのか?
では次に、オブジェクトプールでどれだけパフォーマンスが良くなるのかを軽く検証してみたのでその結果を掲載しておきます。
実験条件
今回、下のGIFのように大量のスフィアを生成→削除する場合にどうなるかを試しました。
- 生成数:1秒あたり3000個
- 各スフィアの生存期間:0.5秒
検証結果
下記が実行時のCPU負荷を記録したグラフです(オブジェクトプール以外の処理も含まれます)。
結論から言うと今回の実験条件ではそこまで大きな違いにはならなかったようです(※正直もう少し違いが出ると思っていただけに「あれ?」という感じでした)。ただし
- 最悪値を比較してみると、オブジェクトプールを使った場合のほうが少し高速だった
- グラフを比較すると未使用の場合のほうがスパイク(=グラフの突起部分)の落差が激しい。オブジェクトプールを使用した場合のほうが負荷が平均化されているように見える。
といった感じでしょうか。もしかしたら実際のゲームだと結果が違ってくるかもしれないので、オブジェクトプールの有効性を詳しく知りたい方は別途ご自身で検証していただければと思います。
Unityでオブジェクトプールを簡単に実装する方法
さて前置きが長くなってしまいましたがここからが本題です。ここではUnity2021から標準で使える機能を使ってオブジェクトプールの実装方法を説明していきますね。
Unity2021から使えるオブジェクトプールのAPIについて
まず、既に書いているようにUnity2021からオブジェクトプール機能が標準で使えるようになりました。詳細は下記の公式マニュアルをご覧いただきたいのですが…
使い方はちょっと変わっていて、ObjectPoolをnewするときに次のように引数に自前で用意した関数をいくつか渡します。
pool = new ObjectPool<GameObject>(OnCreatePooledObject, OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject);
- 第1引数:ゲームオブジェクトの生成処理の関数
- 第2引数:オブジェクトプールからゲームオブジェクトを取得する処理の関数
- 第3引数:ゲームオブジェクトをオブジェクトプールに返却する処理の関数
- 第4引数:ゲームオブジェクトを削除する処理の関数
繰り返しになりますが引数として渡す関数は自分で用意する必要がありますのでご注意ください(ここが一番わかりづらい部分ですね)。あとは
- pool.Get:オブジェクトプールからゲームオブジェクトを借りる
- pool.Release:ゲームオブジェクトをオブジェクトプールに返却する
といった関数が用意されているのでそれを使ってゲームオブジェクトを借りたり返却したりすればOKです。
オブジェクトプールのC#スクリプト
ではAPIの概要をご理解いただいたところでオブジェクトプール実装用のC#スクリプトのサンプルを掲載します。実装にあたっては次の3つのスクリプトが必要です。
- オブジェクトプール管理用スクリプト
- ゲームオブジェクト生成用スクリプト
- ゲームオブジェクト削除用スクリプト
オブジェクトプール管理用スクリプト
using UnityEngine; using UnityEngine.Pool; public class PoolManager : MonoBehaviour { ObjectPool<GameObject> pool; public GameObject Prefab { get; private set; } void Awake() { pool = new ObjectPool<GameObject>(OnCreatePooledObject, OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject); } GameObject OnCreatePooledObject() { return Instantiate(Prefab); } void OnGetFromPool(GameObject obj) { obj.SetActive(true); } void OnReleaseToPool(GameObject obj) { obj.SetActive(false); } void OnDestroyPooledObject(GameObject obj) { Destroy(obj); } public GameObject GetGameObject(GameObject prefab, Vector3 position, Quaternion rotation) { Prefab = prefab; GameObject obj = pool.Get(); Transform tf = obj.transform; tf.position = position; tf.rotation = rotation; return obj; } public void ReleaseGameObject(GameObject obj) { pool.Release(obj); } }
指定したプレハブをプールするためのC#スクリプトです。
ゲームオブジェクト生成用スクリプト
using System.Collections; using UnityEngine; public class Spawner : MonoBehaviour { [SerializeField] bool useObjectPool = true; [SerializeField] PoolManager poolManager; [SerializeField] GameObject prefab; [SerializeField] int spawnCount = 1; [SerializeField] float spawnInterval = 0.1f; [SerializeField] Vector3 minSpawnPosition = Vector3.zero; [SerializeField] Vector3 maxSpawnPosition = Vector3.zero; [SerializeField] float destroyWaitTime = 3; WaitForSeconds spawnIntervalWait; void Start() { spawnIntervalWait = new WaitForSeconds(spawnInterval); StartCoroutine(nameof(SpawnTimer)); } IEnumerator SpawnTimer() { int i; while (true) { for(i = 0; i < spawnCount; i++) { Spawn(prefab); } yield return spawnIntervalWait; } } void Spawn(GameObject prefab) { Destroyer destroyer; Vector3 pos = new Vector3(Random.Range(minSpawnPosition.x, maxSpawnPosition.x), Random.Range(minSpawnPosition.y, maxSpawnPosition.y), Random.Range(minSpawnPosition.z, maxSpawnPosition.z)); if(useObjectPool) { destroyer = poolManager.GetGameObject(prefab, pos, Quaternion.identity).GetComponent<Destroyer>(); destroyer.PoolManager = poolManager; } else { destroyer = Instantiate(prefab, pos, Quaternion.identity).GetComponent<Destroyer>(); } if(destroyer != null) { destroyer.StartDestroyTimer(destroyWaitTime); } } }
指定したプレハブを一定間隔で生成するためのサンプルスクリプトです。オプションでオブジェクトプールを使わず普通にInstantiateできるので検証にお使い下さい。
ゲームオブジェクト削除用スクリプト
using System.Collections; using UnityEngine; public class Destroyer : MonoBehaviour { public PoolManager PoolManager { get; set; } public void StartDestroyTimer(float time) { StartCoroutine(DestroyTimer(time)); } IEnumerator DestroyTimer(float time) { yield return new WaitForSeconds(time); if(PoolManager != null) { PoolManager.ReleaseGameObject(gameObject); } else { Destroy(gameObject); } } }
予めプレハブにアタッチしておくと、指定した時間が経過したタイミングでゲームオブジェクトを消去するサンプルスクリプトです。先ほどのSpawnerと併用することを想定しています。
おわりに
以上、Unity2021から使えるオブジェクトプールの実装方法をご紹介しました。
オブジェクトプールが標準機能として搭載されたのは嬉しいことですが、一方で先ほどの検証結果を見る限りでは効果は微妙な気がしますし、正直ちょっと使いづらい印象があることも否定できません。ただまあそういう機能があることを知っておくと便利だと思いますので、ぜひ上記の内容を参考にしていただければと思います。
この記事がUnityでのゲーム開発のお役に立てば幸いです。