For whatever reason, Pooling Systems are one of those things I have strong opinions on.
I’m not entirely sure how that happened. I think mostly from seeing so many implementations which are just clones of the top search results for “Pooling in Unity Tutorial” and those tutorials being… not great.
Don’t get me wrong, they’re fantastic at explaining the concept of a pool, and even why you might want to use them. But as with many tutorials, they don’t show you what a production-ready implementation of a Pooling System might look like, which I feel is a mistake. The kind of people reading these tutorials (and this series!) are likely to be junior developers who often struggle to make that leap from concept to production ready, and as much as there is benefit in learning by trying and failing at something, sometimes you just need a helping hand to tell you why something is failing.
That’s what I plan to do here. Show my work and explain why I make the choices I do, so you can decide for yourself if you care about a specific line of thinking.
As a bonus by doing it this way, while this series focuses on Unity, it should also help you apply these concepts to other game engines.
Methodology
There’ll be a few times in this series where I mention spikes I’ve seen in the past, unfortunately, I won’t be able to show you them directly as they’re under NDA. But I can recreate similar setups, with some vaguely realistic data, even if just to give you an idea of what to look for in your own profiling.
However, in the interests of my own sanity, these setups will have to be quite simple. They then probably wouldn’t cause an issue on any modern PC or even previous-generation consoles. Luckily for me, I have a piece of hardware lying around which will still show some decent spikes in the profiler in these simple setups!
I’ll be using an Amazon Fire HD 8 (10th Gen) tablet. The Fire 8s are popular tablets for parents to buy for their kids, so there are a good few in the wild. It’s far from a powerhouse, so it should show performance issues relatively easily. It also has the added bonus of being directly applicable to anyone making kids’ games to release on the Amazon App Store.
Profiler information will be captured using official Unity tooling. Either the built-in profiler or via the Profile Analyzer and Memory Profiler packages.
So what even is a pool?
In the most basic terms, a pool allows for re-using an object instead of allocating (and eventually destroying) a new one.
When talking about them in Unity, we’re usually talking about GameObject
pools, although it applies to ScriptableObjects
just as well. Instantiating and Destroying GameObjects
are pretty heavy operations, re-using objects we already made is a good way of avoiding that cost. This is what we’ll focus on here.
Other good candidates for pooling are temporary collections. Lists
, Dictionaries
, StringBuilders
(not a collection per se, but let’s go with it for now), etc which you might need for the odd algorithm here and there, but would rather not create a new one every time.
All the objects I’ve mentioned will also create garbage, which must be collected once freed. Those garbage collection spikes can be painful on lower-end hardware.
We’ll be focusing on GameObjects
for this series, but we might come back to some of the others at a later date.
And what is a “Pooling System”?
Through this series, I’ll use the term “Pool” when I suppose what I really mean is “Pooling System”, and I think that is where a lot of my issues with the existing tutorials lie.
In my mind, a pool handles a single object type, and a Pooling System handles multiple. But also in my mind, a Pooling System is what most Unity projects actually need.
When pooling concrete types, there isn’t much of a separation. You can use C# Generics or similar to get access to a pool of a specific type, and it’s pretty painless. Here is how you might get a List of Vector3s from a static pool, for example:
var points = Pool<List<Vector3>>.Get();
But what about prefab instances? Their type is always GameObject
so you can’t differentiate based on that. Just pool on a specific component type then? Well no, because Pool<ParticleSystem>.Get()
doesn’t solve our problem either. How do we differentiate between Muzzle Flashes, Bullet Impacts and Explosions? They’re all ParticleSystems!
A Pooling System should solve this problem. It’s probably built on top of multiple “pools” and should be able to select the correct one based on the specific object being requested.
Why would I want to use them?
Performance. It’s always performance in game development, right? We only have 16ms to play with every frame, and it’s not like it’s any easier to call Pool.Instantiate
vs just Instantiate
. With all the config and recycle specific code needed for some of the more complex objects, it’s definitely more work.
In any sufficiently complex game, Instantiating objects (or allocating reference types in general) leads to frame spikes. Either immediately, or in a few seconds when the garbage collector kicks in. We’re looking to avoid that.
Why create a whole new missile (containing transforms, logic, meshes, rigidbody, vfx, audio, etc.) just to destroy it in 3 seconds? You should keep a hold of it. Next time you fire a missile, you can check to see if you already have one lying around first, and use that instead.
You might have just saved 3ms! (An actual profiling result I’ve seen in the past for a bullet Instantiation on a PlayStation 4). More likely you’ll have saved about 0.2ms instead. But you’ll save 0.2ms for spawning the bullet, another 0.2ms for the impact vfx, maybe another for the muzzle flash and you’ll push that 2ms+ garbage collection cycle out much later. It all adds up.

Notice the use of “sufficiently complex” a few paragraphs ago, though. Not every game has to care about pooling. As much as every game which allocates at runtime will have frame spikes, those slow frames might still be hitting your target framerate. If that’s the case, then pooling will not help your game perform any better. It will help you consume fewer resources though, leading to better battery life/lower power draw, and cooler temps. I’d still recommend you use them, it’s just not as critical for you to do so.
Pools and pool-like things
There are a few different ways we can set up a pool which we should talk about, before getting into the specifics of how to make one. There’s even an example of something that’s definitely not a pool, but you should consider using it anyway!
Per-Game/Global Pools
Probably the most common kind of pool you’re likely to see, Global Pools initialise themselves while your game is booting up, and clean themselves up when your game shuts down.
If you already have an established project, with Instantiations and Destructions littered all over the place, and you need to start implementing pooling to save some milliseconds, this is what I’d recommend.
Being global you can put a static interface on it, and feed all your instantiations through it, making them the easiest to use and replace what is already in your code.
Some games will have no choice but to use this style of pooling. Open World games for example don’t have much opportunity to clear out and re-initialise their pools. Other games might need one of these pools for some things, but not others. Think UI effects which are used in both the main menu and in-game.
Pros
- Easy to use/integrate
- Fast level load times
- Saves loading and unloading the same things repeatedly.
Cons
- Slower game boot times
- There has to be enough in the pool for the worst-case
- One level needs 50 explosions, but most only need 10? Well, you need 50.
- This memory is always being taken up.
- All your enemies will be in memory on your title screen.
- If a pooled object ends up in a bugged state, it will be in that bugged state every time it is reused.
Per-Level Pools
The next most common setup. These pools are initialised while the game is running and cleared out at some other time in the future. Usually synced up with level lifetime.
This solves several of the issues you’ll find with a global pool, at the expense of a little more thought required on the management side.
You can still put a static interface around these pools, you just need to be careful it’s not called before it’s been set up.
Pros
- Pretty easy to use
- Fast boot times
- Can be tailored to the level being loaded
- No need for leaf VFX in a desert level
- No need for 50 explosions if you’ll only ever need 5
- The longest a bugged object can be a problem is until the end of a level.
Cons
- Slower level load times
- Difficult to apply to all game types
- Constantly loading and unloading the same assets.
- Extra bookkeeping is needed to manage lifetime and access.
Pre-Allocation Only
A close cousin of a pool would be to pre-allocate all of the instances of an object you’ll need ahead of time and never re-use them. This is fantastic because some objects are just plain hard (or worse, expensive!) to reset back to an initial state, particularly objects which might use a third-party plugin you don’t control.
Needless to say this won’t work for most things you might need to pool. How could you know ahead of time how many muzzle flash effects you’re going to need to spawn? You’ll need one of the other pool types to work alongside this one.
There are however some dynamic Instantiations you want to avoid where the number you’ll need is pretty well defined. A wave-based shooter knows exactly how many enemies it will need to spawn when the level is being initialised, so it can set up the “pool” to have that many instances in there and never have to re-use them.
Why use a pooling system for this at all? Why should the spawner not just spawn the instances it needs, and deactivate them? The answer is that there are some additional optimisations we can perform if everything goes through a shared system. We’ll cover this in more detail later in the series.
Pros
- No re-use logic needed
- Fast boot times
- Only instantiate the things you need
- Highly customised to the level being loaded
Cons
- Can waste a few resources
- Does your level need 100 of a certain enemy type, but only ever have 5 active at a time?
- With re-use, you’d only need 5. With this, you’d need 100.
- Can’t handle most objects you’d need to pool
- Can be difficult to know ahead of time how many you’ll need.
Local Pools
If an object (let’s say a gun) fully owns its own, unique pool of objects (bullets in this example), I’d refer to that as a local pool. I generally don’t recommend these, however, the example of a gun holding a pool of its own bullets is a decent use case, if the amount of guns active in the world is not easy to know ahead of time.
They can have all the same functionality as the previous pools mentioned, but they come with their own drawbacks.
Pros
- Setup can be fully customised to the object being spawned.
- The pooling concept doesn’t leak into the rest of the code base.
- If you only have issues dynamically spawning in a small number of places, maybe you don’t want a global system.
Cons
- Completely unbounded in how many instances might exist at once.
- For bullets this might be required.
- But for VFX you might want a global hard limit on how many exist.
- How to handle the case where the owner of the pool is also a spawned object?
- Might conflict with certain other methods of pooling.
Adaptive vs Fixed-Size
At some point when setting up a pool, you’ll come across the issue of what to do when your pool is empty, but you’re trying to spawn another instance.
The obvious choice, and I’d say should be your default setup, is to dynamically allocate an extra instance and log that it has happened. For quite a lot of pooled objects, this is all you can do. It is especially useful during development when you don’t know how big your pools should be.
The other options revolve around a fixed-size pool, where you either return nothing at all or forcefully recycle the oldest living instance. Both of these options are useful for inconsequential juice or VFX, but I’d say recycling the oldest is the one with wider appeal.
But why should they be an option at all? Why not just dynamically increase the pool size for everything? Well… performance! You probably don’t need 1200 impact VFX active when you’re firing a mini-gun. You probably wouldn’t visually notice if you had 100 and recycled the oldest one when you ran out, but you’d notice the difference in the profiler!
So which one?
I’ve hinted at it a few times, but nothing says you should only use one of these options. Mix and match! Use the best option for the particular use case at hand!
A level-based game can still use a global pool for some things. An open-world game can still use the occasional per-level pool. A gun with a local pool for bullets can still use a global pool for its muzzle flash and bullet casings.
If you’re doing this for the first time, or you have an already established project, I’ll stand by my advice to use one or more global pools, accessed via a static interface, to completely replace all dynamic instantiation.
But on a fresh project, where you have full control over what’s happening? You can think about how best to use pooling from the ground up, and that might mean using more than one style of pool to get the best results.
Next…
In the next post, we’ll cover what a good pool looks like in Unity specifically, and implement the basics we’ll need.