A high-performance Object Pool system specifically designed for use with Unity. It utilizes a sparse set, allowing for fast operations on the pooled data, even with very large pool sizes.
- Unity 2020.1 or newer (Only tested with 2021.3)
That's it! Only the demos and tests require some official Unity packages...
- Official Unity Packages:
- Unity.TextMeshPro
- Unity.Burst (Optional)
- Unity.InputSystem (Optional)
- Official Unity Packages:
- Unity.TestRunner
- Download latest package release from here: Releases
- Either:
- Drag the package into the Unity Editor
- Select from the toolbar:
Assets->Import Package->Custom Package
- Select what files you want from the package (eg. you can exclude the
Demosandtestsfolders) - If all goes well, the editor should recompile assemblies without errors!
The main objectives for this asset was for an efficient way to pre-allocate + store high quantities of objects and easily take unused/disabled objects from the pool for (re)use, rather than (re)instantiating and destroying the same object type over and over, which causes additional GC allocations + CPU time (due to C#'s GC and Unity's C# <-> C++ interop).
The object pool uses a sparse set as an efficient data structure for determining what objects are enabled or disabled. It's designed to be adaptable to various needs via events and custom callbacks. It provides an extended class specifically for gameobjects, with optional syncing for the pool's node and gameobject's enabled state.
This example shows how you can achieve a bullet spawner via two different approaches, the more traditional + easy to setup GameObject Pool Approach and the more efficient, data-oriented Generic Object Pool Approach. The demo has bullets that update their position based on their velocity and there is a pool of these bullets from which they're spawned by the player.
This approach is more typical of what developers expect an object pool to do in the context of Unity and for good reason; GameObject's benefit a great deal by being pooled due to their high Instantiate() and Destroy() costs, both in terms of CPU time and GC allocations.
Here we pool a bullet prefab and spawn one whenever the player presses the "spacebar".
// Bullet.cs
using UnityEngine;
public class Bullet : MonoBehaviour
{
public Vector3 vel;
private void OnEnable()
{
vel = Random.insideUnitSphere.normalized;
}
private void FixedUpdate()
{
transform.position += vel * Time.fixedDeltaTime;
// ...
// Other Logic, eg. despawning after certain amount of time
// This can be achieved by calling `gameObject.SetActive(false)`
// if the `GameObjectPool` has both `useMonoHook` and `monoDisablesNode`
// set to true (they both are by default)
}
}
// BulletSpawner.cs
using UnityEngine;
using eilume.ObjectPool;
public class BulletSpawner : MonoBehaviour
{
public GameObject bulletPrefab;
private GameObjectPool _pool;
private void Awake()
{
// Add pool to this gameobject
_pool = gameObject.AddComponent<GameObjectPool>();
_pool.ObjectToPool = bulletPrefab;
// Set the initial size of the pool to 1000
_pool.Setup(1000);
// Fill the pool with the set amount of nodes (1000)
_pool.Fill();
}
// We get and free pooled objects in `FixedUpdate()` for consistent
// results independent of the render framerate (`Update()`)
private void FixedUpdate()
{
// Enable and setup bullet when the 'spacebar' is pressed and
// not all the nodes in the pool are active/already in use
if (Input.GetKeyDown(KeyCode.Space) && !_pool.AllActive)
{
// Get a currently unused node's data from the pool and
// set it to used
GameObject bulletGameObj = _pool.GetData();
}
}
}This approach is more inline with the data-oriented paradigm, as we don't actually spawn any new gameobjects but instead have all the bullet's data purely in memory. This requires more code, but has drastically more significant performance gain opportunities when having large amounts of nodes active at once. We can do more optimized physics queries and optimized rendering via instancing. We can also utilize the Unity Job System (with Burst!) this way, and even potentially move large amounts of processing onto the GPU via a compute shader.
using UnityEngine;
using eilume.ObjectPool;
public class BulletSpawner : MonoBehaviour
{
// In-memory bullet data
private class Bullet
{
public Vector3 pos;
public Vector3 vel;
// ...
// Other Custom Data
public void ApplyVelocity(float delta)
{
pos += vel * delta;
}
}
// Define class inheriting from `ObjectPool` with custom Bullet
// type, as Unity doesn't support generic `MonoBeaviour`s
private class ObjectPoolBullet : ObjectPool<Bullet> { }
private ObjectPoolBullet _pool;
private void Awake()
{
// Add pool to this gameobject
_pool = gameObject.AddComponent<ObjectPoolBullet>();
// Set the initial size of the pool to 1000
_pool.Setup(1000);
// Fill the pool with the set amount of nodes (1000)
_pool.Fill();
}
// We get and free pooled objects in `FixedUpdate()` for consistent
// results independent of the render framerate (`Update()`)
private void FixedUpdate()
{
// Enable and setup bullet when the 'spacebar' is pressed and
// not all the nodes in the pool are active/already in use
if (Input.GetKeyDown(KeyCode.Space) && !_pool.AllActive)
{
// Get a currently unused node's data from the pool and
// set it to used
Bullet bullet = _pool.GetData();
// Reset bullet to initial values
bullet.vel = Random.insideUnitSphere.normalized;
}
// Get all active bullets from the pool and iterate on them
ObjectPoolBullet.Node[] activeBullets = _pool.GetActiveNodes();
for (int i = 0; i < activeBullets.Length; i++)
{
ObjectPoolBullet.Node node = activeBullets[i];
// Apply velocity to each active bullet
node.data.ApplyVelocity(Time.fixedDeltaTime);
// ...
// Custom Logic, eg. physics checks
bool shouldFree = false;
if (shouldFree)
{
// Free the node from the pool, allowing it to be re-used
_pool.Free(node);
}
}
// ...
// Custom Rendering Code, eg. `Graphics.DrawMeshInstancedIndirect()`
}
}Allows for various operations to be chained in a row.
// Create a pool, set the initial size to 5, fill it and then resize pool
// to 10, for future filling and then store the pool in `pool`
var pool = gameObject.AddComponent<ObjectPoolInt>().Setup(5).Fill().Resize(10);var pool = gameObject.AddComponent<ObjectPoolInt>();
// Synchronous is the default, so setting `fillMethod` is optional
pool.fillMethod = GameObjectPool.FillMethod.Sync;
pool.Setup(100).Fill();var pool = gameObject.AddComponent<ObjectPoolInt>();
pool.fillMethod = GameObjectPool.FillMethod.Async;
// Trigger fill to trigger every render update (`Update()`)
pool.fillAsyncTiming = GameObjectPool.FillAsyncTiming.EveryUpdate;
// We set the target fill amount to be the target for each frame
pool.asyncTarget = GameObjectPool.FillAsyncTarget.PerFrame;
// Set target fill amount per tick (in this case, per frame due to above)
pool.TargetFill = 1;
// This will take 100 `Update()`s to fill the pool fully
pool.Setup(100).Fill();