Welcome to another blog post about the trick that stops your game from stuttering every time the player holds down the fire button: object pooling in Unity. If you've ever built a shooter, a bullet hell, or anything that spawns lots of short-lived objects, you've probably felt it. Everything runs smoothly, and then the action heats up and the frame rate starts hiccuping.
Let's see what it is and how we can implement it to improve your frame rate!
Why Instantiate And Destroy Hurt Your Frame Rate
Remember how easy spawning felt in the prefabs article? Instantiate a bullet, Destroy it two seconds later, done. For a handful of objects, that's perfectly fine. For a machine gun, it's a problem.
Instantiating is real work. Unity allocates memory, builds the object and all its components, and even runs Awake and OnEnable. on every single one of them! Destroying leaves behind memory that has to be cleaned up later. That cleanup is called garbage collection in Unity, and when it kicks in, your game can freeze for a moment. Players feel it as a tiny hitch, usually right in the middle of the action.
Nobody has ever said, "I love how my game freezes whenever I shoot." (At least to my knowledge) Let's fix that.
Object Pooling In Unity: The Core Idea
The fix is almost too simple. Instead of creating and destroying objects over and over, create a batch up front, switch them off, and reuse them. Need a bullet? Grab an inactive one and switch it on. Done with it? Switch it off and put it back.
pool (inactive objects) → Get(): switch one on → use it → Release(): switch it off → back in the pool
That's object pooling in Unity. Once the pool is warmed up, there's no Instantiate and no Destroy inside your gameplay loop, so there's nothing new for the garbage collector to clean up.
Building A Simple Pool From Scratch
Here's a minimal pool for bullets. It uses a Queue, a collection where the first item in is the first item out, which suits pooling nicely:
using System.Collections.Generic;
using UnityEngine;
public class BulletPool : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private int startSize = 20;
private readonly Queue<Bullet> pool = new Queue<Bullet>();
void Awake()
{
for (int i = 0; i < startSize; i++)
{
Bullet bullet = CreateBullet();
bullet.gameObject.SetActive(false);
pool.Enqueue(bullet);
}
}
public Bullet Get(Vector3 position, Quaternion rotation)
{
Bullet bullet = pool.Count > 0 ? pool.Dequeue() : CreateBullet();
bullet.transform.SetPositionAndRotation(position, rotation);
bullet.gameObject.SetActive(true);
return bullet;
}
public void Release(Bullet bullet)
{
bullet.gameObject.SetActive(false);
pool.Enqueue(bullet);
}
private Bullet CreateBullet()
{
Bullet bullet = Instantiate(bulletPrefab, transform);
bullet.Init(this);
return bullet;
}
}
Three things happen here. Awake pre-creates a batch and switches them all off. Get pulls one from the queue, or creates a new one if the pool ran dry. Release switches a bullet off and puts it back. Now the bullet needs to know where home is:
public class Bullet : MonoBehaviour
{
private BulletPool pool;
public void Init(BulletPool owner)
{
pool = owner;
}
void OnEnable()
{
Invoke(nameof(ReturnToPool), 2f); // auto-return after 2 seconds
}
void OnDisable()
{
CancelInvoke();
}
void ReturnToPool()
{
pool.Release(this);
}
}
In your shooter, firing becomes a single call: bulletPool.Get(muzzle.position, muzzle.rotation). No more Instantiate, no more Destroy.
The Unity ObjectPool Class: Pooling Without Writing Your Own
Since Unity 2021.1, you don't have to build the pool yourself. The Unity ObjectPool class lives in UnityEngine.Pool and handles the bookkeeping. You just tell it what to do when an object is created, taken, returned, or destroyed:
using UnityEngine;
using UnityEngine.Pool;
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
private ObjectPool<Bullet> pool;
void Awake()
{
pool = new ObjectPool<Bullet>(
createFunc: () =>
{
Bullet bullet = Instantiate(bulletPrefab);
bullet.SetPool(pool);
return bullet;
},
actionOnGet: bullet => bullet.gameObject.SetActive(true),
actionOnRelease: bullet => bullet.gameObject.SetActive(false),
actionOnDestroy: bullet => Destroy(bullet.gameObject),
defaultCapacity: 20,
maxSize: 100
);
}
public void Fire(Vector3 position, Quaternion rotation)
{
Bullet bullet = pool.Get();
bullet.transform.SetPositionAndRotation(position, rotation);
}
}
The bullet just needs a way to hand itself back:
using UnityEngine;
using UnityEngine.Pool;
public class Bullet : MonoBehaviour
{
private ObjectPool<Bullet> pool;
public void SetPool(ObjectPool<Bullet> owner)
{
pool = owner;
}
public void ReturnToPool()
{
pool.Release(this);
}
}
Two settings worth understanding. defaultCapacity is how much room the pool starts with, and maxSize is its ceiling: if the pool is already full when an object comes back, the extra one is destroyed instead of kept. In the Editor, the pool also catches you releasing the same object twice, a classic pooling bug. For most projects, this is the version to reach for, since it means less code and fewer places to hide bugs.
Pooling Gotchas That Bite
Pooling is simple, but it has a few traps:
- Reset state on reuse. A recycled enemy comes back with its last life's health, speed, and trail. Do your resetting in
OnEnable, which runs every time the object is switched on, notAwake, which only runs once. - Always return objects. A forgotten
Releasemeans the pool keeps creating new objects, and you're back to the original problem. - Never release the same object twice. It ends up in the pool two times and gets handed to two different shooters.
- Warm up early. Fill the pool during a loading screen so the first burst of bullets doesn't cause its own hitch.
- Don't
Destroypooled objects by hand. Hand them back instead.
To Pool Or Not To Pool...
Pooling adds code, and code adds bugs. Don't pool the boss that spawns once per level. Pool the things you create constantly: bullets, particle effects, damage numbers, wave enemies, and sound effect sources. If you're not sure you have a problem, open the Profiler and look before you build anything.
So Which Approach Should You Actually Use?
Here's the no-fluff version, the gut check you can run through in your head:
- Spawning something a few times per level → plain
InstantiateandDestroy - Spawning something many times per second → object pooling in Unity
- Unity 2021.1 or newer → the Unity ObjectPool class
- Older Unity, or you want full control → your own
Queuepool - Reused objects behave strangely → reset their state in
OnEnable - Stutters, but you're not sure why → check garbage collection in Unity in the Profiler first
The question that decides it is simple: "how often does this object get created and destroyed?" Rarely means don't bother. Constantly means pool it.
Wrapping Up
Object pooling is one of those techniques that sounds fancy and turns out to be a queue and a couple of SetActive calls. Create objects once, reuse them forever, and your gameplay loop stops triggering garbage collection in Unity. Your players will never notice, which is exactly the point.
~happy Coding!

No comments:
Post a Comment