ドキュメント

Unity Engine

Unity Engine

オブジェクトのプーリングと再利用

UnityのUnityEngine.Pool APIを使用することで、CPUロードとガベージ コレクターのオーバーヘッドを削減できます。これにより、頻繁に使用されるオブジェクトを繰り返し作成/破棄するのではなく、それらをプールして再利用できます。
読み終わるまでの所要時間 24 分最終更新 3日前

オブジェクトプールと再利用は、頻繁に使用されるオブジェクトのインスタンスをプールに返し、そこから取得して再使用できるプログラミングパターンです。オブジェクトプールは、新しいオブジェクトインスタンスのインスタンス化と破棄を繰り返すことによって発生するオーバーヘッドを削減し、割り当てと割り当て解除の全体的な数を制限します。これは、ガベージコレクション(GC)オーバーヘッドの最小化とCPUのロード軽減に役立ちます。
GameObjects はプールできますが、配列、リスト、ディクショナリなどのコレクションタイプなど、頻繁に使用する他のタイプもプールできます。独自のプーリング ロジックを作成することもできますが、Unity は便宜上、
UnityEngine.Pool
名前空間に、Pooling GameObjects といくつかのコレクション タイプのための実装を備えた API のセットを提供します。
注
他の多くのコア Unity API と同様に、
UnityEngine.Pool
API はスレッドセーフではなく、メインスレッドからのみ安全に呼び出すことができます。

Pooling and reusing GameObjects (ゲームオブジェクトのプールと再利用)

発射物など、ゲーム内のオブジェクトの中には、シーン内に同時に数個しか存在しないものであっても、繰り返し頻繁に登場するものがあります。発射されるたびにプレハブから新しい砲弾オブジェクトをインスタンス化する代わりに、ゲームの開始時に砲弾オブジェクトのプールを作成し、必要に応じて再利用できます。

オブジェクトプールの作成

Unity の
ObjectPool<T>
クラスは、再利用可能なオブジェクトプールパターンの実装を提供し、アプリケーションにオブジェクトプールを実装する最も簡単な方法です。
ObjectPool<T>
のインスタンスは、
T
型のオブジェクトのプールを管理します。
ObjectPool<T>
コンストラクタ
を使用して
ObjectPool<T>
を作成する場合は、プールの以下のキーを定義するためのパラメーターを指定します。
  • プールの開始容量。
  • プールを拡張できる最大容量。
  • 同じオブジェクト インスタンスをプールに二重解放しないようにコレクションの安全性チェックを実行するかどうか。
  • 一連の C#
    Action<T>
    デリゲート。Unity がプールで以下のキーアクションを実行するときに呼び出すコールバックです。
    • プールからオブジェクトを取得するときに呼び出すコールバックバック
    • オブジェクトをプールに返すときに呼び出すコールバックバック
    • 新しいオブジェクトインスタンスがリクエストされ、プールが空の場合に、インスタンスの作成時に呼び出されるコールバック
    • オブジェクトインスタンスを破棄するときに呼び出されるコールバック

プールからのオブジェクトの取得

ObjectPool<T>.Get
を呼び出してプールからオブジェクトを取得します。このアクションでは、
ObjectPool
コンストラクタに
actionOnGet
パラメーターとして指定したメソッドも起動します。これにより、使用するオブジェクトを準備してアクティブ化できます。
プールが空のときにプールからオブジェクトをリクエストすると、新しいオブジェクトインスタンスが作成されます。作成プロセスは、
ObjectPool
コンストラクターに
createFunc
パラメーターとして指定したメソッドを呼び出します。
ObjectPool<T>.Get
には過負荷
Get(out PooledObject<T> pooledObject)
があります。
PooledObject
が破棄されると、インスタンスは自動的に解放されます。これは短命のスコープに役立ちます。
詳細とコード例については、
PooledObject<T>.Get
API リファレンス
を参照してください。

プールへのオブジェクトの返却

終了したオブジェクトは、プールに返します。引数として返すオブジェクトとともに
ObjectPool<T>.Release
を呼び出します。このアクションでは、
ObjectPool
コンストラクタに
actionOnRelease
パラメーターとして指定したメソッドも実行されます。このメソッドは、返されたオブジェクトの非アクティブ化とステートのリセットを可能にします。
プールがいっぱいになったときにオブジェクトをプールに戻すと、オブジェクトは破棄されます。破棄プロセスは、
ObjectPool
コンストラクタに
actionOnDestroy
パラメーターとして指定したメソッドを呼び出します。
コンストラクターに
collectionCheck
パラメーターとして
true
を渡すと、コレクションの安全性チェックが有効になります。コレクション チェックが有効になっているときに、すでにプールにあるオブジェクトを返そうとした場合、Unity は例外をスローします。有効にした場合でも、コレクションの確認はエディターでのみ可能で、プレイヤービルドでは使用できません。コレクション チェックを有効にするとオーバーヘッドが増えますが、ダブル リリース エラーがキャッチされるため、開発時に使用することをお勧めします。

使用済みオブジェクトをリセットする

プールから取得したオブジェクトは、使用中にステートを変更できます。例えば、プールされた敵ゲームオブジェクトは、シーン内でアクティブなときに、その位置、体力、アニメーションステート、物理演算ステートを変更できます。オブジェクトがプールに返されたときにこのステートをリセットすることが重要です。そうすることで、後で再度取得したときに、最初に使用したのと同じステートで開始されます。
リセットするステートに応じて、
actionOnGet
デリゲートのプールからオブジェクトを取得したとき、または
actionOnRelease
デリゲートのプールにリリースして戻すときに、オブジェクトのリセットを選択できます。プールされたオブジェクト自体にカスタムの非アクティブ化メソッドを実装し、プールの取得またはリリースアクションからそれらのメソッドをコールこともオプションます。
リセットが必要なステートは、プールされたオブジェクトの特定の動作とコンポーネントによって異なります。リリース時の一般的なリセットタスクには、コルーチンの停止、イベントのサブスクライブ解除、物理演算状態のリセット、アニメーションのクリア、パーティクルシステムの停止などがあります。リリース時に GameObjects を非アクティブにして、プールで非アクティブの間に
Update
呼び出しを受けないようにすることも重要です。

例: 砲弾のプール

砲弾用の
ObjectPool<T>
の一般的な実装には、以下のキーステージがあります。
  1. ゲームの開始時に非アクティブの砲弾オブジェクトのプールを作成します。プールされたオブジェクトの新規作成、プールからのオブジェクトの取得、プールへのオブジェクトの解放、プールが満杯になったオブジェクトの破棄時にコールバックとして呼び出すために必要なデリゲートをコンストラクターに提供します。
  2. 発射物が必要な場合 (例えば、ユーザーが武器を発射する場合)、
    ObjectPool<T>.Get
    を使用してプールから非アクティブの発射物を取得します。対応する
    actionOnGet
    コールバックから、取得したオブジェクトをアクティブ化し、位置や向きなどの関連プロパティを初期化します。
  3. 砲弾が不要になったとき (例えば、ターゲットに当たったとき、または範囲外になったとき)、
    ObjectPool<T>.Release
    とともにプールに戻します。発射体には専用のプールへのリファレンスを渡すのが一般的です。そうすることで、発射後に自身をリリースできます。対応する
    actionOnRelease
    コールバックから、返されたオブジェクトを非アクティブにし、速度やアニメーションステートなどの関連プロパティをリセットします。
以下のコードは、
ObjectPool<T>
にプールされた砲弾のスクリプトコンポーネントコードです。砲弾には、それ自体のプールへのリファレンスを与える public プロパティがあります。このプロパティは、時間遅延後に自身をプールにリリースするために使用されます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Pool; public class RevisedProjectile :MonoBehaviour { // Deactivate after delay [SerializeField] private float timeoutDelay = 3f; private IObjectPool<RevisedProjectile> objectPool; // Public property to give the projectile a reference to its ObjectPool public IObjectPool<RevisedProjectile> ObjectPool { set => objectPool = value; } public void Deactivate() { StartCoroutine(DeactivateRoutine(timeoutDelay)); } IEnumerator DeactivateRoutine(float 遅延) { yield return new WaitForSeconds(遅延); // Reset the moving Rigidbody Rigidbody rBody = GetComponent<Rigidbody>(); rBody.linearVelocity = new Vector3(0f, 0f, 0f); rBody.angularVelocity = new Vector3(0f, 0f, 0f); // Release the projectile back to the pool objectPool.Release(this); } }
以下のコードは、砲塔にアタッチされたコンポーネントのコードで、砲弾の発射と砲弾オブジェクトのプールの管理を行います。ユーザーが射撃したときに、オブジェクトプールの
Get
が呼び出され、プールされた砲弾が取得されます。
using UnityEngine; using UnityEngine.Pool; using UnityEngine.Events; public class RevisedGun :MonoBehaviour { [Tooltip("撃つプレハブ ) [SerializeField] private RevisedProjectile projectilePrefab; [ツールチップ("発射力")] [SerializeField] private float muzzleVelocity = 1500f; [ツールチップ("ショットが表示される銃の終了位置")] [SerializeField] private Transform muzzlePosition; [Tooltip("射撃間隔 / 小さい = 発射率が高い")] [SerializeField] private float cooldownWindow = 0.1f; [SerializeField] private UnityEvent m_GunFired; // Stack-based ObjectPool available with Unity 2021 and above private IObjectPool<RevisedProjectile> objectPool; // Throw an exception if we try to return an existing item, already in the pool [SerializeField] private bool collectionCheck = true; // Extra options to control the pool capacity and maximum size [SerializeField] private int defaultCapacity = 20; [SerializeField] private int maxSize = 100; private float nextTimeToShoot; private void Awake() { objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile, OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject, collectionCheck, defaultCapacity, maxSize); } // Invoked when creating an item to populate the object pool private RevisedProjectile CreateProjectile() { RevisedProjectile projectileInstance = Instantiate(projectilePrefab); projectileInstance.ObjectPool = objectPool; return projectileInstance; } // Invoked when returning an item to the object pool private void OnReleaseToPool(RevisedProjectile pooledObject) { pooledObject.gameObject.SetActive(false); } // Invoked when retrieving the next item from the object pool private void OnGetFromPool(RevisedProjectile pooledObject) { pooledObject.gameObject.SetActive(true); } // Invoked when the maximum number of pooled items is exceeded (i.e. destroy the pooled object) private void OnDestroyPooledObject(RevisedProjectile pooledObject) { Destroy(pooledObject.gameObject); } private void FixedUpdate() { // Invokes the Shoot method if the delay time is exceeded if (Input.GetButton("Fire1") && Time.time > nextTimeToShoot && objectPool != null) { Shoot(); } } private void Shoot() { // Get a pooled object instead of instantiating RevisedProjectile bulletObject = objectPool.Get(); if (bulletObject == null) return; // Align to gun barrel/muzzle position bulletObject.transform.SetPositionAndRotation(muzzlePosition.position, muzzlePosition.rotation); // Move projectile forward bulletObject.GetComponent<Rigidbody>().AddForce(bulletObject.Transform.フォワード * muzzleVelocity, ForceMode.Acceleration); // Turn off after a few seconds bulletObject.Deactivate(); // Set cooldown delay nextTimeToShoot = Time.time + cooldownWindow; m_GunFired.Invoke(); } }
このコードを使用した完全なサンプルプロジェクトは、必要なすべてのプレハブとプールされた砲弾を発射する作業用タレットを含めて、アセットストア)から無料のパッケージとしてダウンロードできます。詳細については、『 オブジェクトプール使用による Unity での C# スクリプトのパフォーマンス向上 』を参照してください。

プールされたオブジェクトのシーンアンロードの処理

デフォルトでは、オブジェクトプールは作成されたシーンに関連付けられています。そのシーンがアンロードされると、Unity はそのシーン内のすべての GameObjects (プールとプールされたインスタンスを含む) を破棄します。
シーンのリロード後にオブジェクトプールを維持するには、
object.DontDestroyOnLoad
を呼び出すルートゲームオブジェクトの下にプールマネージャーを配置します。
transform.SetParent
を
呼び出すと、プールルートの下にプールされた項目をリリース時に親にして、シーンで破棄しないようにすることができます。
アンロード時にシーン内にプールされたアクティブなインスタンスがある場合は、それらが
OnDisable
と
OnDestroy
でリリースされているか、アイテムを呼び出すか破棄する
SceneManager.activeSceneChanged
コールバックが登録されていることを確認してください。
非アクティブの場合、プールされたインスタンスへの長期参照を避けます。
ObjectPool<T>.Clear
を使用して、例えばレベル間で非アクティブなインスタンスをすべて破棄します。

コレクションタイプのプーリングと再利用

また、Lists や Dictionaries など、
System.Collection
名前空間から収集オブジェクトをプールして再利用することもできます。
コレクションクラスは、コレクションの値を削除しますが、コレクションに割り当てられたメモリはリリースしない
Clear
メソッドを公開します。これは複雑な計算のために一時的なヘルパーCollectionsを割り当てたい場合に便利です。
以下のコード例では、フレームごとに 1 回
nearestNeighbors
List
を割り当てて、一連のデータ ポイントを収集しています。これは非効率な実践方法です。
// Bad C# script example. This Update method allocates a new `List` every frame.void Update() { List<float> nearestNeighbors = new List<float>(); findDistancesToNearestNeighbors(nearestNeighbors); nearestNeighbors.Sort(); // … use the sorted list somehow …}
代わりに、この
List
をメソッドの外、包含するクラスに持ち上げ、そのたびに消去して再利用できます。これにより、コードがフレームごとに新しい
List
を割り当てる必要がなくなります。
// Good C# script example. This method re-uses the same List every frame.List<float> m_NearestNeighbors = new List<float>();void Update() { m_NearestNeighbors.Clear(); findDistancesToNearestNeighbors(NearestNeighbors); m_NearestNeighbors.Sort(); // … use the sorted list somehow …}
このサンプル コードでは、
List
インスタンスのメモリを複数のフレームで保持して再利用しています。コードは、
List
の拡張が必要なときのみ新しいメモリーを割り当てます。
UnityEngine.Pool
名前空間では、
List<T>
、
HashSet<T>
、
Dictionary<TKey, TValue>
などのいくつかのコレクション タイプ向けに既製のプール実装も提供されます。これらは
ObjectPool<T>
と同じ原理で動作し、プールされたコレクションオブジェクトインスタンスを必要に応じて取得およびリリースできます。詳細と例については、
CollectionPool<T>
および同じ名前空間の他のコレクション タイプ固有のプール クラスに関する API リファレンス ドキュメントを参照してください。

追加リソース