Created
December 31, 2025 08:29
-
-
Save somedeveloper00/a950ae0734c84ac15380db4094a1f57a to your computer and use it in GitHub Desktop.
Simple prefab/game-object pooling system for Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Runtime.CompilerServices; | |
| using UnityEngine; | |
| using UnityEngine.Assertions; | |
| using UnityEngine.Events; | |
| namespace Common | |
| { | |
| /// <summary> | |
| /// Works alongside <see cref="PrefabPool"/>. Attach this to a <see cref="GameObject"/> that you want to pool. | |
| /// </summary> | |
| public class Poolable : MonoBehaviour | |
| { | |
| [Tooltip("Gets invoked when this game object is retrieved from the pool.")] | |
| public UnityEvent onGet; | |
| [Tooltip("like onGet but executed in the next yield")] | |
| public UnityEvent onGetLate; | |
| [Tooltip("Delay (in seconds) to release this game object on demand. Could be used to wait for an animation to finish before being released. onRelease gets invoked on demand, then waits for this delay, then gets released.")] | |
| public float releaseDelay = 0; | |
| [Tooltip("Gets invoked when this game object is released from the pool. (before the delay is awaited)")] | |
| public UnityEvent onRelease; | |
| [NonSerialized] | |
| public PrefabPool pool; | |
| public bool deactivateGameObject = true; | |
| void Awake() => HandleDeactivateGameObject(); | |
| void OnValidate() => Assert.IsTrue(!deactivateGameObject || gameObject.activeSelf, "if deactivateGameObject is true, the game object must be active"); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Release() | |
| { | |
| Assert.IsNotNull(pool); | |
| pool.Release(this); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| void HandleDeactivateGameObject() | |
| { | |
| if (deactivateGameObject) | |
| { | |
| onGet.AddListener(() => gameObject.SetActive(true)); | |
| onRelease.AddListener(() => gameObject.SetActive(false)); | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Runtime.CompilerServices; | |
| using System.Threading.Tasks; | |
| using UnityEngine; | |
| using UnityEngine.Assertions; | |
| using UnityEngine.Pool; | |
| namespace Common | |
| { | |
| /// <summary> | |
| /// Like <see cref="PrefabPool"/> but as a component | |
| /// </summary> | |
| public class PrefabPoolComponent : MonoBehaviour, IObjectPool<GameObject> | |
| { | |
| public int initialCapacity = 0; | |
| public PrefabPool prefabPool; | |
| void Start() | |
| { | |
| prefabPool.EnsureParentExists(); | |
| for (int i = 0; i < initialCapacity; i++) | |
| prefabPool.AddToPool(Instantiate(prefabPool.prefab, prefabPool.parent)); | |
| } | |
| public int CountInactive => prefabPool.CountInactive; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Clear() => prefabPool.Clear(); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GameObject Get() => prefabPool.Get(); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Poolable GetPoolable() => prefabPool.GetPoolable(); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public PooledObject<GameObject> Get(out GameObject v) => prefabPool.Get(out v); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Release(GameObject element) => prefabPool.Release(element); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Task ReleaseAsync(GameObject element) => prefabPool.ReleaseAsync(element); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Release(Poolable poolable) => prefabPool.Release(poolable); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Task ReleaseAsync(Poolable poolable) => prefabPool.ReleaseAsync(poolable); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void SpawnAtNoParent(Transform parent) => prefabPool.SpawnAtNoParent(parent); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void SpawnAtNoParent(Transform parent, out Poolable poolable) => poolable = prefabPool.SpawnAtNoParent(parent); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void SpawnAt(Transform parent) => prefabPool.SpawnAt(parent); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void SpawnAt(Transform parent, out Poolable poolable) => poolable = prefabPool.SpawnAt(parent); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Poolable SpawnAt(Vector3 pos) => prefabPool.SpawnAt(pos); | |
| } | |
| /// <summary> | |
| /// Simple pooling implementation that moves objects off-screen and places them with an offset from each other. | |
| /// </summary> | |
| [Serializable] | |
| public class PrefabPool : IObjectPool<GameObject> | |
| { | |
| public Poolable prefab; | |
| [Tooltip("Minimum time (in seconds) to have to wait before releasing an object. Requests in the meantime will be ignored (unless maxInDelay is > 0). Useful for vfx.")] | |
| public float minDelay = 0; | |
| [Tooltip("If minDelay is set, this will be used in the meantime of the delay as a maximum threshold. Requests beyond this threshold in the meantime will be ignored.")] | |
| public int maxInDelay = 0; | |
| public Vector3 offScreen = new(1000, 1000, 1000); | |
| public Vector3 offset = new(1000, 1000, 1000); | |
| [NonSerialized] | |
| public Transform parent; | |
| readonly List<Poolable> _pool = new(16); | |
| float _lastRequestTime = 0; | |
| int _requestsInDelay = 0; | |
| public int CountInactive => _pool.Count; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void AddToPool(Poolable poolable) | |
| { | |
| poolable.pool = this; | |
| _pool.Add(poolable); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Clear() | |
| { | |
| if (parent != null) | |
| UnityEngine.Object.Destroy(parent.gameObject); | |
| _pool.Clear(); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GameObject Get() => GetPoolable().gameObject; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Poolable GetPoolable() | |
| { | |
| if (minDelay > 0) | |
| { | |
| if (Time.time - _lastRequestTime < minDelay) | |
| { | |
| if (++_requestsInDelay > maxInDelay) | |
| return null; | |
| } | |
| else | |
| { | |
| _lastRequestTime = Time.time; | |
| _requestsInDelay = 1; | |
| } | |
| } | |
| EnsureParentExists(); | |
| while (_pool.Count > 0) | |
| { | |
| var obj = _pool[^1]; | |
| _pool.RemoveAt(_pool.Count - 1); | |
| if (obj != null) | |
| { | |
| obj.onGet?.Invoke(); | |
| if (obj.onGetLate != null) | |
| ExecuteNextFrame.Add(obj.onGetLate); | |
| return obj; | |
| } | |
| } | |
| Assert.IsNotNull(prefab); | |
| var newObj = UnityEngine.Object.Instantiate(prefab, parent); | |
| newObj.pool = this; | |
| newObj.onGet?.Invoke(); | |
| if (newObj.onGetLate != null) | |
| TaskSafe.Start(async () => | |
| { | |
| await Task.Yield(); | |
| if (newObj) | |
| newObj.onGetLate?.Invoke(); | |
| }); | |
| return newObj; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public PooledObject<GameObject> Get(out GameObject v) | |
| { | |
| var obj = GetPoolable(); | |
| v = obj ? obj.gameObject : null; | |
| return new PooledObject<GameObject>(v, this); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Release(GameObject element) => _ = ReleaseAsync(element); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Task ReleaseAsync(GameObject element) | |
| { | |
| Assert.IsNotNull(element); | |
| return ReleaseAsync(element.GetComponent<Poolable>()); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Release(Poolable poolable) => _ = ReleaseAsync(poolable); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public async Task ReleaseAsync(Poolable poolable) | |
| { | |
| EnsureParentExists(); | |
| Assert.IsNotNull(poolable); | |
| poolable.onRelease?.Invoke(); | |
| if (poolable.releaseDelay > 0) | |
| await TaskSafe.Delay((long)(poolable.releaseDelay * 1000)); | |
| if (poolable != null) | |
| { | |
| poolable.transform.position = offScreen + offset * _pool.Count; | |
| poolable.transform.SetParent(parent, false); | |
| _pool.Add(poolable); | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Poolable SpawnAtNoParent(Transform parent) | |
| { | |
| var obj = GetPoolable(); | |
| if (obj != null) | |
| { | |
| parent.GetPositionAndRotation(out var pos, out var rot); | |
| obj.transform.SetPositionAndRotation(pos, rot); | |
| } | |
| return obj; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Poolable SpawnAt(Transform parent) | |
| { | |
| parent.GetPositionAndRotation(out var pos, out var rot); | |
| var obj = GetPoolable(); | |
| if (obj) | |
| { | |
| var transform = obj.transform; | |
| transform.SetPositionAndRotation(pos, rot); | |
| transform.SetParent(parent); | |
| } | |
| return obj; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Poolable SpawnAt(Vector3 pos) | |
| { | |
| var obj = GetPoolable(); | |
| if (obj != null) | |
| obj.transform.position = pos; | |
| return obj; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void EnsureParentExists() | |
| { | |
| if (!parent) | |
| parent = new GameObject($"{prefab.name}(Pool)").transform; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment