31
27

Unity向けオブジェクトプールライブラリ「uPools」

Posted at

uPoolsとは

uPoolsとはUnity向けのオブジェクトプールライブラリです。
汎用的なオブジェクトプールから非同期オブジェクトプールや、既存実装をほぼそのままオブジェクトプール化できる機能が搭載されています。

※ 本記事では「UniTask」を使用しています。

サンプルコード

導入方法

導入方法はGitHubのREADMEを参考にしてください。

機能紹介

SharedGameObjectPool

SharedGameObjectPoolはPrefabの生成と破棄をとても簡単にオブジェクトプール化する機能です。
PrefabのInstantiate()SharedGameObjectPool.Rent()に置き換え、Destroy()SharedGameObjectPool.Return()に置き換えるだけでPrefabをオブジェクトプールで管理できるようになります。

  1. PrefabであるGameObjectをSharedGameObjectPool.Prewarmで登録し、プールの初期サイズも指定する
  2. SharedGameObjectPool.Rent() でオブジェクトをプールから取り出す(座標指定やコンポーネント指定も可能)
  3. 使い終わったらSharedGameObjectPool.Return()で返却する

PrewarmGameObjectを登録し同時にプールの初期サイズを指定する必要があります。
この登録時にプールの初期サイズまで一斉にPrefabがインスタンス化され、プールに格納されます。
もしプールされたオブジェクト数以上に要求した場合はそのタイミングで新しくインスタンス化されます。

GameObjectをSharedGameObjectPoolで管理する
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using uPools;
using Random = UnityEngine.Random;

namespace Samples.SharedGameObjectPools
{
    public class ObjectSpawnerA : MonoBehaviour
    {
        [SerializeField] private GameObject _prefabA;

        private void Start()
        {
            // SharedGameObjectPool は staticクラスのためインスタンス化不要
            // Prewarmで事前にPrefabの存在を登録する必要がある
            SharedGameObjectPool.Prewarm(_prefabA, 2);

            // PrefabAを生成し続ける
            CreatePrefabALoopAsync(destroyCancellationToken).Forget();
        }

        private async UniTaskVoid CreatePrefabALoopAsync(CancellationToken ct)
        {
            while (!ct.IsCancellationRequested)
            {
                // PrefabAをプールからレンタルする
                var a = SharedGameObjectPool.Rent(
                    _prefabA,
                    Random.insideUnitSphere + Vector3.up * 5,
                    Quaternion.identity);

                // 適当に利用して終わったら返却する
                UseAsync(a, ct).Forget();

                // 1秒待って再生成する
                await UniTask.Delay(1000, cancellationToken: ct);
            }
        }

        private async UniTaskVoid UseAsync(GameObject obj, CancellationToken ct)
        {
            // 1.5秒経ったら返却する
            await UniTask.Delay(1500, cancellationToken: ct);

            // 返却する
            SharedGameObjectPool.Return(obj);
        }
    }
}

s1.gif

なお、オブジェクトプールを用いる場合はOnEnable()OnDisable()、または後述するIPoolCallbackReceiverを用いてオブジェクトの初期化や片付けを行う必要があります。
コンポーネント作成時は注意しましょう。

オブジェクトプールで管理されることを考慮したコンポーネント
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Random = UnityEngine.Random;

namespace Samples.SharedGameObjectPoolSamples
{
    public class TimeCounter : MonoBehaviour
    {
        private CancellationTokenSource _cancellationTokenSource;
        public Action OnFinished { get; set; }


        // プールからレンタルされたらEnableされる
        private void OnEnable()
        {
            _cancellationTokenSource?.Cancel();
            _cancellationTokenSource?.Dispose();

            _cancellationTokenSource = new CancellationTokenSource();
            
            WaitAsync(_cancellationTokenSource.Token).Forget();
        }

        // プールに返却されたらDisableされる
        private void OnDisable()
        {
            _cancellationTokenSource?.Cancel();
            _cancellationTokenSource?.Dispose();
            OnFinished = null;
        }

        // 一定時間経ったら通知する
        private async UniTaskVoid WaitAsync(CancellationToken ct)
        {
            try
            {
                await UniTask.Delay(TimeSpan.FromSeconds(Random.Range(1, 5)), cancellationToken: ct);
            }
            finally
            {
                OnFinished?.Invoke();
            }
        }
    }
}
コンポーネントを意識したオブジェクトプールの利用
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using uPools;
using Random = UnityEngine.Random;

namespace Samples.SharedGameObjectPoolSamples
{
    public class ObjectSpawnerB : MonoBehaviour
    {
        [SerializeField] private TimeCounter _timeCounterPrefab;

        private void Start()
        {
            // SharedGameObjectPool は staticクラスのためインスタンス化不要

            // 登録するのものは「GameObject」である必要がある
            SharedGameObjectPool.Prewarm(_timeCounterPrefab.gameObject, 2);

            // 生成し続ける
            CreatePrefabBLoopAsync(destroyCancellationToken).Forget();
        }
        
        private async UniTaskVoid CreatePrefabBLoopAsync(CancellationToken ct)
        {
            while (!ct.IsCancellationRequested)
            {
                // プールからレンタルする
                // 型推論される
                TimeCounter timeCounter = SharedGameObjectPool.Rent(
                    _timeCounterPrefab,
                    Random.insideUnitSphere + Vector3.up * 5,
                    Quaternion.identity);

                timeCounter.OnFinished += () =>
                {
                    // PrefabBの処理が終わったら返却する
                    SharedGameObjectPool.Return(timeCounter.gameObject);
                };

                // 1秒待ってまた生成する
                await UniTask.Delay(1000, cancellationToken: ct);
            }
        }
    }
}

s2.gif

(プールサイズ以上のオブジェクトを要求した場合はそのタイミングで新しく生成されている)

IPoolCallbackReceiverインタフェース

IPoolCallbackReceiverを実装することでRent()/Return()時に呼び出されるコールバックを受け取ることができるようになります。

IPoolCallbackReceiver
namespace uPools
{
    public interface IPoolCallbackReceiver
    {
        void OnRent();
        void OnReturn();
    }
}

たとえば、さきほどのTimeCounterの実装は次に置き換えることもできます。

IPoolCallbackReceiverを用いた例
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using uPools;
using Random = UnityEngine.Random;

namespace Samples.IPoolCallbackReceiverSample
{
    // IPoolCallbackReceiverでプールからレンタルされたときと返却されたときの処理を実装する
    public class TimeCounter : MonoBehaviour, IPoolCallbackReceiver
    {
        private CancellationTokenSource _cancellationTokenSource;
        public Action OnFinished { get; set; }

        // プールからレンタルされたら呼び出される
        public void OnRent()
        {
            _cancellationTokenSource?.Cancel();
            _cancellationTokenSource?.Dispose();

            _cancellationTokenSource = new CancellationTokenSource();

            WaitAsync(_cancellationTokenSource.Token).Forget();
        }

        // プールに返却されたら呼び出される
        public void OnReturn()
        {
            _cancellationTokenSource?.Cancel();
            _cancellationTokenSource?.Dispose();
            _cancellationTokenSource = null;
            OnFinished = null;
        }

        // 一定時間経ったら通知する
        private async UniTaskVoid WaitAsync(CancellationToken ct)
        {
            try
            {
                // ランダムに待つ
                await UniTask.Delay(TimeSpan.FromSeconds(Random.Range(0.5f, 5f)), cancellationToken: ct);
            }
            finally
            {
                OnFinished?.Invoke();
            }
        }

        // 完全に破棄された場合の処理も忘れずに
        private void OnDestroy()
        {
            _cancellationTokenSource?.Cancel();
            _cancellationTokenSource?.Dispose();
            _cancellationTokenSource = null;
        }
    }
}

ObjectPool

ObjectPool<T>は通常のclass(GameObjectではないピュアなC#クラス)をプーリングするためのオブジェクトプールです。
スレッドセーフではないためマルチスレッドな環境で用いる場合は注意してください。

using System;
using UnityEngine;
using uPools;

namespace Samples.ObjectPoolSample
{
    // プールされるオブジェクト
    public sealed class MyObject : IPoolCallbackReceiver, IDisposable
    {
        private static int InstanceCount = 0;

        public int MyId { get; }

        public MyObject()
        {
            MyId = InstanceCount++;
            Debug.Log($"{nameof(MyObject)}[{MyId}]が生成されました");
        }
        
        public void OnRent()
        {
            Debug.Log($"{nameof(MyObject)}[{MyId}]がレンタルされました");
        }

        public void OnReturn()
        {
            Debug.Log($"{nameof(MyObject)}[{MyId}]が返却されました");
        }

        public void Dispose()
        {
            Debug.Log($"{nameof(MyObject)}[{MyId}]がDispose()されました");
        }
    }
}
using UnityEngine;
using uPools;

namespace Samples.ObjectPoolSample
{
    public sealed class ObjectPoolUseSample : MonoBehaviour
    {
        private ObjectPool<MyObject> _objectPool;

        private void Awake()
        {
            Debug.Log("-- Initialize --");

            // プールを作成する
            _objectPool = new ObjectPool<MyObject>(
                // 新規生成時に実行される
                createFunc: () => new MyObject(),
                // プールからレンタルされたときに実行される
                onRent: obj => { Debug.Log($"onRent: MyObject[{obj.MyId}]がプールからレンタルされました"); },
                // プールに返却されたときに実行される
                onReturn: obj => { Debug.Log($"onReturn: MyObject[{obj.MyId}]がプールに返却されました"); },
                // オブジェクトがプールから破棄されたときに実行される
                onDestroy: obj =>
                {
                    Debug.Log($"onDestroy: MyObject[{obj.MyId}]がプールから破棄されました");
                    obj.Dispose();
                });

            Debug.Log("-- Prewarm = 2 --");

            // 事前に生成しておくオブジェクト数を指定
            _objectPool.Prewarm(2);

            Debug.Log("-- Rent & Return --");
            {
                var obj1 = _objectPool.Rent();
                var obj2 = _objectPool.Rent();

                _objectPool.Return(obj1);
                _objectPool.Return(obj2);
            }

            Debug.Log("-- Clear pool --");
            _objectPool.Clear();
            
            Debug.Log("-- Rent & Return --");
            {
                // プールが空になっているので新しく生成される
                var obj1 = _objectPool.Rent();
                _objectPool.Return(obj1);
            }
            
            Debug.Log("-- Dispose --");
            _objectPool.Dispose();
        }
    }
}
ログ(読みやすくちょっと整形)
-- Initialize --

-- Prewarm = 2 --

MyObject[0]が生成されました
onReturn: MyObject[0]がプールに返却されました
MyObject[0]が返却されました

MyObject[1]が生成されました
onReturn: MyObject[1]がプールに返却されました
MyObject[1]が返却されました

-- Rent & Return --

onRent: MyObject[1]がプールからレンタルされました
MyObject[1]がレンタルされました
onRent: MyObject[0]がプールからレンタルされました
MyObject[0]がレンタルされました

onReturn: MyObject[1]がプールに返却されました
MyObject[1]が返却されました
onReturn: MyObject[0]がプールに返却されました
MyObject[0]が返却されました

-- Clear pool --

onDestroy: MyObject[0]がプールから破棄されました
MyObject[0]がDispose()されました
onDestroy: MyObject[1]がプールから破棄されました
MyObject[1]がDispose()されました

-- Rent & Return --

MyObject[2]が生成されました
onReturn: MyObject[2]がプールに返却されました
MyObject[2]が返却されました

-- Dispose --

onDestroy: MyObject[2]がプールから破棄されました
MyObject[2]がDispose()されました

ObjectPool<T>をインスタンス化する際にコールバック登録ができます。
最低限createFuncの登録が必要ですが、可能であればonDestroyも登録してオブジェクトのDispose()にも対応できるとよいでしょう。

_objectPool = new ObjectPool<MyObject>(
    // 新規生成時に実行される
    createFunc: () => new MyObject(),
    // プールから破棄されたときに実行される
    onDestroy: obj => obj.Dispose()
);

GameObjectPool

GameObjectPoolはGameObjectをプーリングするためのオブジェクトプールです。
Prefabを登録しておき、必要に応じてInstantiateされてプーリングされます。
プールに保管されたオブジェクトはDisable状態となり、取り出すタイミングでEnableとなります。

using UnityEngine;
using uPools;

namespace Samples.GameObjectPoolSample
{
    public class GameObjectPoolUseSample : MonoBehaviour
    {
        [SerializeField] private GameObject _prefab;

        private GameObjectPool _pool;

        private void Start()
        {
            // プールの作成時にPrefabを指定する
            _pool = new GameObjectPool(_prefab);

            {
                // オブジェクトをレンタル
                var go = _pool.Rent();
                _pool.Return(go);
            }
            {
                // オブジェクトをレンタル
                // 親Transformを指定
                var go = _pool.Rent(transform);
                _pool.Return(go);
            }
            {
                // オブジェクトをレンタル
                // 親Transformや位置姿勢を指定
                var go = _pool.Rent(new Vector3(0, 10, 0), Quaternion.identity, transform);
                _pool.Return(go);
            }
            
            // プールをクリア(登録されているGameObjectもDestroyされる)
            _pool.Clear();
            
            // Disposeでも内部的にClear()が呼ばれる
            _pool.Dispose();
        }
    }
}

独自のObjectPoolをつくる

ObjectPoolBase<T>を継承することで独自挙動のプールを作成することができます。

using UnityEngine;
using uPools;

namespace Samples.CustomSample
{
    // GameObjectをプールするクラスをカスタマイズしてみる
    public sealed class CustomGameObjectPool : ObjectPoolBase<GameObject>
    {
        private readonly GameObject _prefab;

        public CustomGameObjectPool(GameObject prefab)
        {
            _prefab = prefab;
        }

        protected override GameObject CreateInstance()
        {
            var obj = Object.Instantiate(_prefab);
            // ここで初期化処理を行ったりできる
            return obj;
        }

        protected override void OnDestroy(GameObject instance)
        {
            Object.Destroy(instance);
        }

        protected override void OnRent(GameObject instance)
        {
            // ここでレンタル時の処理を行ったりできる
            instance.SetActive(true);
        }

        protected override void OnReturn(GameObject instance)
        {
            // ここで返却時の処理を行ったりできる
            instance.SetActive(false);
        }
    }
}

非同期版オブジェクトプール

UniTaskを導入している場合はAsyncObjectPool<T>およびAsyncObjectPoolBase<T>が利用可能になります。
こちらはオブジェクト生成時の処理やPrewarmなどが非同期に対応しているのが特徴です。

using System;
using UnityEngine;
using uPools;

namespace Samples.AsyncObjectPoolSample
{
    // プールされるオブジェクト
    public sealed class MyObject : IPoolCallbackReceiver, IDisposable
    {
        private static int InstanceCount = 0;

        public int MyId { get; }

        public MyObject()
        {
            MyId = InstanceCount++;
            Debug.Log($"{nameof(MyObject)}[{MyId}]が生成されました");
        }
        
        public void OnRent()
        {
            Debug.Log($"{nameof(MyObject)}[{MyId}]がレンタルされました");
        }

        public void OnReturn()
        {
            Debug.Log($"{nameof(MyObject)}[{MyId}]が返却されました");
        }

        public void Dispose()
        {
            Debug.Log($"{nameof(MyObject)}[{MyId}]がDispose()されました");
        }
    }
}
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using uPools;
using Random = UnityEngine.Random;

namespace Samples.AsyncObjectPoolSample
{
    public sealed class AsyncObjectPoolUseSample : MonoBehaviour
    {
        private AsyncObjectPool<MyObject> _asyncObjectPool;

        private async UniTaskVoid Start()
        {
            Debug.Log("-- Initialize --");

            // プールを作成する
            _asyncObjectPool = new AsyncObjectPool<MyObject>(
                // 新規生成時に実行される
                createFunc: async ct =>
                {
                    // 非同期処理があったとして
                    await UniTask.Delay(
                        TimeSpan.FromMilliseconds(Random.Range(100, 1000)),
                        cancellationToken: ct);

                    return new MyObject();
                },
                // プールからレンタルされたときに実行される
                onRent: obj => { Debug.Log($"onRent: MyObject[{obj.MyId}]がプールからレンタルされました"); },
                // プールに返却されたときに実行される
                onReturn: obj => { Debug.Log($"onReturn: MyObject[{obj.MyId}]がプールに返却されました"); },
                // オブジェクトがプールから破棄されたときに実行される
                onDestroy: obj =>
                {
                    Debug.Log($"onDestroy: MyObject[{obj.MyId}]がプールから破棄されました");
                    obj.Dispose();
                });

            Debug.Log("-- PrewarmAsync = 2 --");

            // 事前にオブジェクトを非同期に生成
            await _asyncObjectPool.PrewarmAsync(3, destroyCancellationToken);

            Debug.Log("-- RentAsync & Return --");
            {
                var obj1 = await _asyncObjectPool.RentAsync(destroyCancellationToken);
                var obj2 = await _asyncObjectPool.RentAsync(destroyCancellationToken);

                _asyncObjectPool.Return(obj1);
                _asyncObjectPool.Return(obj2);
            }

            Debug.Log("-- Clear pool --");
            _asyncObjectPool.Clear();

            Debug.Log("-- Rent & Return --");
            {
                // プールが空になっているので新しく生成される
                var obj1 = await _asyncObjectPool.RentAsync(destroyCancellationToken);
                _asyncObjectPool.Return(obj1);
            }

            Debug.Log("-- Dispose --");
            _asyncObjectPool.Dispose();
        }
    }
}

Addressables

Addressablesを導入している場合はAddressableGameObjectPoolが利用できます。
また加えてUniTaskを導入している場合はAsyncAddressableGameObjectPoolも利用できます。

こちらはAssetReferenceGameObjectを生成元にしてプールを作成することができます。

まとめ

uPoolsを用いるとUnity向けのオブジェクトプールを比較的楽に用意することができます。
またGameObject以外に適用しての利用も可能であり、カスタマイズも容易です。

31
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
27