はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】Unity公式のObjectPoolを使ってみる(内部実装も一部紹介)

はじめに

今回は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 連結リスト

またObjectPoolCountAllCountInactiveというプロパティが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);
}

ObjectPoolLinkedPoolの箇所で説明したcollectionChecktrue版がGenericPoolfalse版がUnsafeGenericPoolと型で分かれているようです。