はじめに
今回はObjectPool
について紹介していきたいと思います。
The object pool pattern is a software creational design pattern that uses a set of initialized objects kept ready to use – a "pool" – rather than allocating and destroying them on demand. A client of the pool will request an object from the pool and perform operations on the returned object. When the client has finished, it returns the object to the pool rather than destroying it; this can be done manually or automatically.
Object pools are primarily used for performance: in some circumstances, object pools significantly improve performance. Object pools complicate object lifetime, as objects obtained from and returned to a pool are not actually created or destroyed at this time, and thus require care in implementation.
// DeepL翻訳
オブジェクト・プール・パターンとは、オンデマンドでオブジェクトを割り当てたり破壊したりするのではなく、すぐに使えるように初期化されたオブジェクトのセット-「プール」-を使用するソフトウェア作成デザインパターンである。プールのクライアントは、プールからオブジェクトを要求し、返されたオブジェクトに対して操作を実行する。クライアントが終了すると、オブジェクトは破棄されずにプールに戻される。これは、手動または自動で行うことができる。オブジェクトプールは主にパフォーマンスのために使用されます。ある状況では、オブジェクトプールはパフォーマンスを大幅に向上させます。オブジェクトプールはオブジェクトの寿命を複雑にします。プールから取得したオブジェクトとプールに返されたオブジェクトは、実際にはこの時点では作成も破棄もされないため、実装には注意が必要です。
https://en.wikipedia.org/wiki/Object_pool_pattern
簡単に言うと毎回オブジェクトの生成・破棄をするのではなく、使いまわしてパフォーマンスを向上させようというデザインパターンです。
今までは各自がObjectPool
を実装していたのですが、どうやらUnity公式で実装してくれたみたいですね。
公式ドキュメントによるとUnity2021.1
から導入されています。

環境
Unity2021.3.0f1
概要
Unityが実装しているObjectPool
には以下があります。
- ObjectPool<T0>
- LinkedPool<T0>
- CollectionPool<T0,T1>
- DictionaryPool<T0,T1>
- HashSetPool<T0>
- ListPool<T0>
- GenericPool<T0>
- UnsafeGenericPool<T0>
間を開けているのは、少し使い方といいますか性質が異なるのでそうしています。
公式ドキュメントによると全てスレッドセーフではないそうなので注意。
またObjectPool<T0>
とLinkedPool<T0>
はIObjectPool<T0>
というインターフェイスを実装しています。
namespace UnityEngine.Pool { public interface IObjectPool<T> where T : class { int CountInactive { get; } T Get(); PooledObject<T> Get(out T v); void Release(T element); void Clear(); } }
Unity - Scripting API: IObjectPool<T0>
ObjectPool<T0>とLinkedPool<T0>の違い
内部のデータ構造が異なるようですね。
The ObjectPool uses a stack to hold a collection of object instances for reuse and is not thread-safe.
The LinkedPool uses a linked list to hold a collection of object instances for reuse.
型 | データ構造 |
---|---|
ObjectPool | スタック |
LinkedPool | 連結リスト |
またObjectPool
はCountAll
とCountInactive
というプロパティがLinkedPool
と比較して増えてます。詳細は後述。
ObjectPool<T0>
これの使い方をマスターすれば、他のCollection
の奴とかGeneric
の奴とかもマスターしたも同然なので要チェックです。
docs.unity3d.com
コンストラクタのパラメーター | 意味 | |
---|---|---|
createFunc | プールが空のときに新しいインスタンスを生成する処理 | |
actionOnGet | インスタンスがプールから取り出されたときに呼び出される処理 | |
actionOnRelease | インスタンスがプールに戻されるときに呼び出される処理 | |
actionOnDestroy | プールがmaxSize に達した際、要素をプールに戻せなかったときに呼び出される処理 |
|
collectionCheck | プールに戻す際に既に同一インスタンスが登録されているか調べ、あれば例外を投げる。エディタでのみ実行されることに注意 | |
defaultCapacity | スタックのデフォルトの容量 | |
maxSize | プールの最大サイズ。 |
// インスタンス化の例 ObjectPool<GameObject> pool = new ObjectPool<GameObject>( createFunc: () => GameObject.CreatePrimitive(PrimitiveType.Cube), // プールが空のときに新しいインスタンスを生成する処理 actionOnGet: target => target.SetActive(true), // プールから取り出されたときの処理 actionOnRelease: target => target.SetActive(false), // プールに戻したときの処理 actionOnDestroy: target => Destroy(target), // プールがmaxSizeを超えたときの処理 collectionCheck: true, // 同一インスタンスが登録されていないかチェックするかどうか defaultCapacity: 10, // デフォルトの容量 maxSize: 100);
// 利用側 private ObjectPool<GameObject> pool; private void Start() { // プールから取り出す (パターン1) var obj1 = pool.Get(); // プールから取り出す (パターン2) pool.Get(out var obj2); // プールに戻す pool.Release(obj1); // プールで作成されたが、まだ返却されていないオブジェクト数 Debug.Log(pool.CountActive); // アクティブと非アクティブなオブジェクトの合計数 Debug.Log(pool.CountAll); // 現在プールで利用可能なオブジェクト数 Debug.Log(pool.CountInactive); // 全てのプールされているオブジェクトを破棄する pool.Clear(); // プールの破棄(Clearと同等) pool.Dispose(); }
LinkedPool<T0>
ObjectPool
とほぼ同じですが、データ構造が連結リストになっています。
docs.unity3d.com
コンストラクタのパラメーター | 意味 | |
---|---|---|
createFunc | プールが空のときに新しいインスタンスを生成する処理 | |
actionOnGet | インスタンスがプールから取り出されたときに呼び出される処理 | |
actionOnRelease | インスタンスがプールに戻されるときに呼び出される処理 | |
actionOnDestroy | プールがmaxSize に達した際、要素をプールに戻せなかったときに呼び出される処理 |
|
collectionCheck | プールに戻す際に既に同一インスタンスが登録されているか調べ、あれば例外を投げる。エディタでのみ実行されることに注意 | |
maxSize | プールの最大サイズ。 |
// インスタンス化の例 LinkedPool<GameObject> pool = new LinkedPool<GameObject>( createFunc: () => GameObject.CreatePrimitive(PrimitiveType.Cube), // プールが空のときに新しいインスタンスを生成する処理 actionOnGet: target => target.SetActive(true), // プールから取り出されたときの処理 actionOnRelease: target => target.SetActive(false), // プールに戻したときの処理 actionOnDestroy: target => Destroy(target), // プールがmaxSizeを超えたときの処理 collectionCheck: true, // 同一インスタンスが登録されていないかチェックするかどうか maxSize: 100); // プールの最大サイズ );
// 利用側 private LinkedPool<GameObject> pool; private void Start() { // プールから取り出す (パターン1) var obj1 = pool.Get(); // プールから取り出す (パターン2) pool.Get(out var obj2); // プールに戻す pool.Release(obj1); // 現在プールで利用可能なオブジェクト数 Debug.Log(pool.CountInactive); // 全てのプールされているオブジェクトを破棄する pool.Clear(); // プールの破棄(Clearと同等) pool.Dispose(); }
CollectionPoolとDictionaryPoolとHashSetPoolとListPool
コレクションのプールをする際に利用します。実装としてはObjectPool<T>
をコレクション用にstatic
にしたものですね。
// CollectionPoolの例 // プールから取り出す List<int> obj = CollectionPool<List<int>, int>.Get(); // プールに戻す CollectionPool<List<int>, int>.Release(obj); // usingを使ったバージョン (usingを抜けると、自動でプールに戻す) using (CollectionPool<List<int>, int>.Get(out var list)) { }
// DictionaryPoolの例 // プールから取り出す Dictionary<int, int> obj = DictionaryPool<int, int>.Get(); // プールに戻す DictionaryPool<int, int>.Release(obj); // usingを使ったバージョン (usingを抜けると、自動でプールに戻す) using (DictionaryPool<int, int>.Get(out var dictionary)) { }
// HashSetPoolの例 // プールから取り出す HashSet<int> obj = HashSetPool<int>.Get(); // プールに戻す HashSetPool<int>.Release(obj); // usingを使ったバージョン (usingを抜けると、自動でプールに戻す) using (HashSetPool<int>.Get(out var hashSet)) { }
// ListPoolの例 // プールから取り出す List<int> obj = ListPool<int>.Get(); // プールに戻す ListPool<int>.Release(obj); // usingを使ったバージョン (usingを抜けると、自動でプールに戻す) using (ListPool<int>.Get(out var list)) { }
正直中身の実装を見れば大体の挙動は掴めると思います。
namespace UnityEngine.Pool { /// <summary> /// <para>A Collection such as List, HashSet, Dictionary etc can be pooled and reused by using a CollectionPool.</para> /// </summary> public class CollectionPool<TCollection, TItem> where TCollection : class, ICollection<TItem>, new() { internal static readonly ObjectPool<TCollection> s_Pool = new ObjectPool<TCollection>((Func<TCollection>) (() => new TCollection()), actionOnRelease: ((Action<TCollection>) (l => l.Clear()))); public static TCollection Get() => CollectionPool<TCollection, TItem>.s_Pool.Get(); public static PooledObject<TCollection> Get(out TCollection value) => CollectionPool<TCollection, TItem>.s_Pool.Get(out value); public static void Release(TCollection toRelease) => CollectionPool<TCollection, TItem>.s_Pool.Release(toRelease); } }
namespace UnityEngine.Pool { /// <summary> /// <para>A version of Pool.CollectionPool_2 for Dictionaries.</para> /// </summary> public class DictionaryPool<TKey, TValue> : CollectionPool<Dictionary<TKey, TValue>, KeyValuePair<TKey, TValue>> { } }
namespace UnityEngine.Pool { /// <summary> /// <para>A version of Pool.CollectionPool_2 for HashSets.</para> /// </summary> public class HashSetPool<T> : CollectionPool<HashSet<T>, T> { } }
namespace UnityEngine.Pool { /// <summary> /// <para>A version of Pool.CollectionPool_2 for Lists.</para> /// </summary> public class ListPool<T> : CollectionPool<List<T>, T> { } }
実装を見ての通りCollectionPool
で全てを表現でき、それぞれジェネリックの型を指定して使いやすくしてくれているのがそれ以外のやつです。
ただ利用する際にs_Pool
のインスタンスが型によって異なることは注意です。
GenericPoolとUnsafeGenericPool
ObjectPool<T0>
のstatic
バージョンです。CollectionPool
よりも汎用的なものですね。
細かい説明よりも、実装を見た方が簡単に理解できるかもしれません
namespace UnityEngine.Pool { /// <summary> /// <para>Provides a static implementation of Pool.ObjectPool_1.</para> /// </summary> public class GenericPool<T> where T : class, new() { internal static readonly ObjectPool<T> s_Pool = new ObjectPool<T>((Func<T>) (() => new T())); public static T Get() => GenericPool<T>.s_Pool.Get(); public static PooledObject<T> Get(out T value) => GenericPool<T>.s_Pool.Get(out value); public static void Release(T toRelease) => GenericPool<T>.s_Pool.Release(toRelease); } }
namespace UnityEngine.Pool { /// <summary> /// <para>Provides a static implementation of Pool.ObjectPool_1.</para> /// </summary> public static class UnsafeGenericPool<T> where T : class, new() { internal static readonly ObjectPool<T> s_Pool = new ObjectPool<T>((Func<T>) (() => new T()), collectionCheck: false); public static T Get() => UnsafeGenericPool<T>.s_Pool.Get(); public static PooledObject<T> Get(out T value) => UnsafeGenericPool<T>.s_Pool.Get(out value); public static void Release(T toRelease) => UnsafeGenericPool<T>.s_Pool.Release(toRelease); } }
// 利用方法 private class Sample { public int value; } private void Start() { // プールから取り出す var obj = GenericPool<Sample>.Get(); // プールに戻す GenericPool<Sample>.Release(obj); }
ObjectPool
・LinkedPool
の箇所で説明したcollectionCheck
のtrue
版がGenericPool
、false
版がUnsafeGenericPool
と型で分かれているようです。