uPoolsとは
uPoolsとはUnity向けのオブジェクトプールライブラリです。
汎用的なオブジェクトプールから非同期オブジェクトプールや、既存実装をほぼそのままオブジェクトプール化できる機能が搭載されています。
※ 本記事では「UniTask」を使用しています。
サンプルコード
導入方法
導入方法はGitHubのREADMEを参考にしてください。
機能紹介
SharedGameObjectPool
SharedGameObjectPool
はPrefabの生成と破棄をとても簡単にオブジェクトプール化する機能です。
PrefabのInstantiate()
をSharedGameObjectPool.Rent()
に置き換え、Destroy()
をSharedGameObjectPool.Return()
に置き換えるだけでPrefabをオブジェクトプールで管理できるようになります。
- PrefabであるGameObjectを
SharedGameObjectPool.Prewarm
で登録し、プールの初期サイズも指定する -
SharedGameObjectPool.Rent()
でオブジェクトをプールから取り出す(座標指定やコンポーネント指定も可能) - 使い終わったら
SharedGameObjectPool.Return()
で返却する
Prewarm
でGameObject
を登録し同時にプールの初期サイズを指定する必要があります。
この登録時にプールの初期サイズまで一斉にPrefabがインスタンス化され、プールに格納されます。
もしプールされたオブジェクト数以上に要求した場合はそのタイミングで新しくインスタンス化されます。
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);
}
}
}
なお、オブジェクトプールを用いる場合は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);
}
}
}
}
(プールサイズ以上のオブジェクトを要求した場合はそのタイミングで新しく生成されている)
IPoolCallbackReceiverインタフェース
IPoolCallbackReceiver
を実装することでRent()
/Return()
時に呼び出されるコールバックを受け取ることができるようになります。
namespace uPools
{
public interface IPoolCallbackReceiver
{
void OnRent();
void OnReturn();
}
}
たとえば、さきほどのTimeCounter
の実装は次に置き換えることもできます。
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以外に適用しての利用も可能であり、カスタマイズも容易です。