Generic Object Pooling
The Object Pooling pattern is a great way to manage memory and reduce garbage collection spikes.
Today, we will build a generic object pooling system and use it inside a shooting system example.
But before starting we should know Why are new and Destroy so bad for performance?
Frequently creating (Instantiate) and destroying objects like rapid fire bullets causes two major problems for your game's performance:
- Creation is Heavy: Every time you create a new object, the CPU has to search for free memory and initialize the object. Doing this dozens of times per frame will quickly lower your framerate.
- Destruction Causes Lag Spikes: When you
Destroyan object in C#, the memory isn't freed instantly; it becomes garbage. Eventually, the Garbage Collector (GC) has to completely pause your game to clean up this mess. This sudden pause is exactly what causes those annoying frame drops or stutters during heavy gameplay.
That is where the Object Pooling pattern comes in. It simply involves deactivating the object when you are done with it, putting it in a pool, and reactivating it the next time you need one.
Implementing UnityEngine.Pool
Since Unity 2021, we don’t need to write a pooling manager from scratch. Unity provides a highly optimized, generic system built right into the UnityEngine.Pool namespace. It enforces clean architecture by requiring us to define exactly what happens when an object is created, requested, returned, and destroyed.
Let’s implement this with two scripts: a Weapon that manages the pool, and a Bullet that knows how to release itself.
Get Abdullah’s stories in your inbox
Join Medium for free to get updates from this writer.
1. The Bullet just needs a return address. We use the SetPool() method to pass it a reference to the pool it was born in. When it hits a wall (or its lifetime expires), it simply calls _pool.Release(this). This is the exact equivalent of calling Destroy(), but instead of trashing the memory, it neatly returns itself to the pool manager.
using UnityEngine;
using UnityEngine.Pool;
public class Bullet : MonoBehaviour
{
private IObjectPool<Bullet> _pool;
private float _lifeTime = 2f;
public void SetPool(IObjectPool<Bullet> pool)
{
_pool = pool;
}
private void OnEnable()
{
Invoke(nameof(ReturnToPool), _lifeTime);
}
private void OnDisable()
{
CancelInvoke();
}
private void ReturnToPool()
{
_pool.Release(this);
}
private void OnCollisionEnter(Collision collision)
{
ReturnToPool();
}
}2. The Weapon makes the rules. The magic happens inside the Weapon's Awake() method. When we create the new ObjectPool<Bullet>, we have to define four rules:
- Create: How to build a brand new bullet if the pool is empty.
- Take: What to do when a bullet is requested (turn it on with
SetActive(true)). - Return: What to do when a bullet comes back (turn it off with
SetActive(false)). - Destroy: What to do if the pool gets too full and we actually do need to delete one.
using UnityEngine;
using UnityEngine.Pool;
public class Weapon : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private float shootForce = 20f;
[SerializeField] private float spawnOffset = 1.0f;
[SerializeField] private bool collectionCheck = true;
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxSize = 100;
private IObjectPool<Bullet> _bulletPool;
private void Awake()
{
_bulletPool = new ObjectPool<Bullet>(
CreateBullet, OnTakeBulletFromPool, OnReturnBulletToPool, OnDestroyBullet,
collectionCheck, defaultCapacity, maxSize);
}
private void Update()
{
if (Input.GetButtonDown("Fire1"))
{
Shoot();
}
}
private void Shoot()
{
Bullet newBullet = _bulletPool.Get();
Vector3 safeSpawnPosition = transform.position + (transform.forward * spawnOffset);
newBullet.transform.position = safeSpawnPosition;
newBullet.transform.rotation = transform.rotation;
Rigidbody rb = newBullet.GetComponent<Rigidbody>();
if (rb != null)
{
rb.linearVelocity = Vector3.zero;
rb.AddForce(transform.forward * shootForce, ForceMode.Impulse);
}
}
//The 4 Core Pooling Functions
private Bullet CreateBullet()
{
Bullet bulletInstance = Instantiate(bulletPrefab);
bulletInstance.SetPool(_bulletPool);
return bulletInstance;
}
private void OnTakeBulletFromPool(Bullet bullet)
{
bullet.gameObject.SetActive(true);
}
private void OnReturnBulletToPool(Bullet bullet)
{
bullet.gameObject.SetActive(false);
}
private void OnDestroyBullet(Bullet bullet)
{
Destroy(bullet.gameObject);
}
}Once those rules are set, shooting becomes as simple as calling _bulletPool.Get(). The pool automatically handles the rest!
Wrapping Up
Object pooling is a must-have pattern for high performance game development. By taking advantage of the built-in UnityEngine.Pool namespace, you save yourself from writing custom managers from scratch while protecting your game from those dreaded garbage collection lag spikes.
While we only built a shooting system today, the beauty of this being a generic pool is that you can apply this exact same code to anything: spawning enemies, popping up damage numbers, or handling complex UI elements.
What system are you going to optimize with Object Pooling next? Let me know in the comments below, and if this guide helped you, feel free to leave a clap. Thanks for reading










