Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
In this blog post I will show you how to use object pooling in Unity 2021 with this new official API so you don't have to mess with 3rd party code that breaks on every Unity upgrade.
Check the original blog post at The Gamedev Guru: Object Pooling in Unity 2021+
While developing your game, you noticed that instantiating 100 bullets per second is suffocating your mobile CPU performance. You then...
A: you reduce the number of bullets to 20
B: you implement your own pooling system
C: you pay 50 bucks for a pooling system in the Asset Store
D: you use the new Unity Pooling API introduced in 2021
In this blog post, we are going to cover this last option.
Today, you will learn how to use the new Pooling API introduced in 2021.
Table of Content
Since Unity 2021, you have access to a wealthy set of pooling features that will help you develop high-performing Unity projects.
Ready to get to know them?
Let’s start with the most important question: when do you need pooling?
I ask that because pooling shouldn’t be your go-to solution 24/7.
Object pooling in Unity has definitely some important drawbacks that do more harm than good, so you have to be careful there.
We will look into those later.
To keep it short, consider pooling when:
You instantiate and destroy gameobjects in rapid succession, e.g. weapon bullets.
You frequently allocate and deallocate objects stored in the heap (instead of reusing them). That includes C# collections.
These operations cause lots of allocations and therefore:
A waste of CPU cycles for instantiation and destroy operations (or new/dispose).
Premature garbage collections that end in these gameplay freezes your players hate
Memory fragmentation that make it hard to find free contiguous memory regions
Do you feel like those problems can pose a threat for you?
(If not now, they might later)
Let’s keep going.
Now that you know if you are in trouble (or still safe), let me quickly explain what pooling is.
Pooling is a performance optimization technique that is all about reusing C# entities instead of creating and destroying them every time you need them.
An entity can be anything: a game object, an instanced prefab, a C# dictionary, etc..
Let me put the concept of pooling in the context of a real life example.
Let’s say you have to go grocery shopping tomorrow morning.
What do you usually take with you apart from your wallet and keys?
Well, you might take reusable bags. After all, you need some sort of container to bring your rations back home.
So you take your empty reusable bags, fill them with groceries and come back home.
Once you’re home, you empty your bags and put them back into your drawer.
So that’s pool.
Reusable bags are a better alternative than buying (allocating) plastic bags and trashing (deallocating) them every time you go shopping.
You need a bag?
Ok, you go to your pool of bags (e.g. a drawer in the kitchen), you take a few, you use them, you empty them and finally you return them back to the pool.
See what we did there?
Here are the main components of a pooling use case:
The elements you want to pool, e.g. a reusable bag, an instantiated bullet, etc...
The goal you have for these elements, e.g. holding groceries, shooting a bullet, etc..
The functions you perform on the pool and its elements: Take, Return, Reset.
In the case of a shooter game, you can create and destroy bullets every single time... or you can create a bunch beforehand and then reuse them like this:
You spawn a thousand bullets and put them in a pool.
Whenever you fire your weapon, you a bullet from that pool.
When the bullet hits something and disappears, you put it back into the pool.
This way, you save the CPU cycles it takes to instantiate and destroy these prefabs. Plus, you alleviate pressure on the garbage collector.
Now, before you jump right into pooling, be aware of a few points...
The pooling technique has a few (potential) issues:
Your items might be dirty.
Since they were used in the past, you might have left them in an undesirable state, such as bullets with some red juice on them.
That means, you need to spend some CPU cycles to clean your items before using them: the reset operation.
You reserve memory that you might actually not need.
If you pool thousands of bullets but all your player wanted to do is to look at the landscape, then you wasted memory.
It adds complexity to your codebase.
You need to manage the lifecycle of your pools.
This not only adds CPU cycles but brain cycles due to processing a larger codebase.
All you need to do is to avoid pooling in cases where you would not really profit from it.
Like, there’s no need to pool the final boss. It’s just one, after all.
Remember: what matters most is the frequency of your instantiate and destroy operations.
If you do them often, consider pooling. Otherwise, skip it.
We will see more pooling issues in detail later on.
Now, let’s have a look at your options for pooling.
If you want to pool your objects in your Unity project, you have three options:
Make your own system
Buy a third-party pooling system
Import UnityEngine.Pool
Let ’s check them out.
One option is to put your crafting skills into practice.
Implementing your own pooling system doesn’t sound too complex, as you only need to implement a few operations:
Create & dispose pool
Take from the pool
Return into the pool
Reset operations
But it often gets more complicated than that when you start thinking of:
Type safety
Memory management and data structures
Custom object allocation/deallocation
Thread-safety
Does that sound like a headache already?
I can already see your face a bit paler than before...
My suggestion is to not reinvent the wheel (unless it’s a learning exercise).
Since it’s an already-solved problem, use something that works so you can focus on your project.
Focus on bringing the fun to your players.
That’s what they’ll pay you for anyway.
Let’s check the second option.
Here, you choose a 3rd party provider from sources like:
The Unity Asset Store
Github
A friend or family member
Let’s see a few examples:
But before you click the buy button... keep reading.
3rd party tools can work wonders and have tons of features.
But that comes with drawbacks:
You rely on their support to fix issues and upgrade the packages to newer editor versions.
If you don’t have source code, you can’t fix issues yourself.
More features = complex code. It will take you time to understand and maintain their system.
They can get expensive (in money and time).
You probably knew all of that, but it’s always a neat reminder :-)
And nowadays there are even fewer reasons to go for a 3rd party asset, since Unity has quietly released a new pooling API in Unity 2021.
And that’s the main topic of this post.
With version 2021 onwards, Unity released a few C# pooling mechanisms that will help you in a myriad of use cases.
These pools of objects are directly integrated in the Unity engine. No extra downloads required and kept up to date on each Unity update.
As a huge plus, you have access to their source code.
And I must say that the implementations are quite straight forward. It’s a nice evening read.
Let’s see how you can start using the Unity Pooling API today so that you can reduce the performance cost of these operations you and I know about.
The first step is to make sure you are on Unity 2021+.
(I mean, you can just copy & paste the code into any of your older projects... but hey, I never said this)
Then, it’s just a matter of knowing the Unity Pooling API:
The pooling operations.
The different pooling containers.
I already gave you a few spoilers on the pooling operations. But let's dig deeper into them.
The first operation you need to do is to construct the pooling container of your choice.
That’s usually done in a single line of code, so don’t worry here.
The constructor parameters depend on the specific container you want to use, but they are quite similar
Here are the usual Unity pooling constructor parameters:
createFunc | Called to create a new instance of your object, e.g. () => new GameObject(“Bullet”) or () => new Vector3(0,0,0) |
---|---|
actionOnGet | Called when you take an instance the pool, e.g. to activate the game object |
actionOnRelease | Called when you return an instance to the pool, e.g. to clean up and deactivate the instance. |
actionOnDestroy | Called when the pool destroys this item, i.e. when it doesn’t fit (exceeds maximum size) or the pool is destroyed |
collectionCheck | True if you want Unity to check that this item was not already in the pool when you try to return it (only in editor) |
defaultCapacity | Default pool size: initial size of the stack/list that will contain your elements |
maxSize | Pool size: maximum number of free items that are in the pool at any given time. If you return an item to a pool that is full, this item will be destroyed instead. |
Here’s how you can create an object pool of GameObjects:
_pool = new ObjectPool<GameObject>(createFunc: () => new GameObject("PooledObject"), actionOnGet: (obj) => obj.SetActive(true), actionOnRelease: (obj) => obj.SetActive(false), actionOnDestroy: (obj) => Destroy(obj), collectionChecks: false, defaultCapacity: 10, maxPoolSize: 10);
I left the parameter names for clarity; feel free to skip them in production code :-).
And sure enough, this is just an example with a GameObject. You can use it with any type you want.
Okay, now you have a pool of _GameObject_s.
How do you use it?
The first thing Unity needs to know how to create more of your _GameObject_s whenever you request more than are available.
We already specified that in the constructor, as we passed the createFunc function as the first parameter to the pool constructor.
Every time that you want to take a GameObject from an empty pool, Unity will create one for you and give it to you.
And to create it, it will use the createFunc function you passed.
And how do we take a GameObject from the pool?
Now that you have your pool reference stored in _pool, you can call its Get function:
GameObject myGameObject = _pool.Get();
That’s it.
Now you can use the object as you wish (within certain limits).
When you are done with it, you need to return it back to your pool so you can use it later on.
So you have been using your element for a few minutes and now you’re done with it.
What now?
Here is what you do not do now: you do not destroy/dispose it yourself.
Instead, you return it to the pool so that the pool can manage its lifecycle correctly according to the functions you provided.
How do you do that? Easy:
_pool.Return(myObject);
The pool will then:
Call the actionOnRelease function you provided with that element as an argument so you can deactivate it, stop the particle system, etc..
Check if there’s enough space in its internal list/stack based on maxSize:
If there’s enough free space in the container, then put the object in there.
If there is no free space, then destroy the object by calling actionOnDestroy.
That’s it.
And talking about destroying elements...
Whenever you dispose your pool or there is no internal space to store the elements you return, the pool will destroy that element.
And it does so by calling the actionOnDestroy function you passed in its constructor.
This function can be as simple as doing nothing... or calling Destroy(myObject) if we are talking about objects managed by Unity.
And finally, whenever you are done with your pool, you should dispose it.
Disposing your pool is all about freeing up the resources owned by the pool.
There is often a stack or a list inside of your pool that contains the elements that are free to take.
Well, you dispose your pool by calling:
_pool.Dispose();
Okay, so that’s the functionality there is to pooling.
But we are still missing one important bit...
Not all pools are created for the same use case.
Let’s see what pool types Unity offers to cover your needs.
The first group of pools are those that cover generic C# objects (95%+ of the elements you might want to pool).
A typical use case for these type of pools are game objects — whether instantiated from prefabs or not —.
The difference between LinkedPool and ObjectPool is the internal data structure that Unity uses to hold the elements you want to pool.
An ObjectPool simply uses a C# Stack, which uses an C# array underneath:
private T[] _array;
Being a stack, it contains a big chunk of contiguous memory.
Its worst case is having 0 items (length = 0) in a big pool (capacity = 100000). There, you’ll have a big chunk of reserved memory that you’re not using.
Resizing stacks happens when you go over its capacity. And that’s expensive, as you need to allocate a bigger chunk and copy your elements over.
Hint: you can avoid stack resizing by playing with the maxCapacity constructor parameter.
LinkedPool uses a linked list, which may lead to better memory management depending on your case. Here’s how the data structure looks like:
internal class LinkedPoolItem { internal LinkedPool<T>.LinkedPoolItem poolNext; internal T value; }
With LinkedPool, you only use memory for the elements that are actually stored in the pool.
But that comes with an extra cost: you spend more memory per item and more CPU cycles to manage this data structure.
You probably know the differences between arrays and linked lists, anyway.
So let’s talk about the next category of Unity object pooling classes.
Now we are talking about pooling C# collections in Unity.
You see, in game development you most likely need to use lists, dictionaries, hashsets and collections alike.
And often enough, you need to create/destroy these collections frequently.
We do this often in AI when running specific one-off behaviors or algorithms. There, we frequently need supporting data structures to perform searches, evaluations and scoring.
So that’s the thing...
Whenever you create and destroy collections, you put pressure onto the memory management system. That’s because you:
Allocate and deallocate the collection plus its internal data structures.
You possibly resize your collections dynamically.
So a solution that helps with some of these run-time allocations in Unity is collection pooling.
Whenever you need a list, you can just grab it from a pool, use it and return it when done.
Here’s an example:
var manuallyReleasedPooledList = ListPool<Vector2>.Get(); manuallyReleasedPooledList.Add(Random.insideUnitCircle); // Use your pool // ... ListPool<Vector2>.Release(manuallyReleasedPooledList);
And here’s a different construct that releases the collection pool for you:
using (var pooledObject = ListPool<Vector2>.Get(out List<Vector2> automaticallyReleasedPooledList)) { automaticallyReleasedPooledList.Add(Random.insideUnitCircle); // Use your pool // ... }
Whenever you go out of the scope of that using block, Unity will return the list to the pool for you.
CollectionPool is the base class for these specific collections; so if you make your own collections you can create a pool for them by inheriting from that one.
ListPool, DictionaryPool and HashSetPool are specific pools for their respective collection types.
Now, you have to be careful with these collection pools.
I say that, because internally, all of these collection pools from Unity work based on a static pool variable.
Which means...
Using these collection pools will break a feature that improve your iteration times in the editor: disabling domain reload.
If you use static pools like these carelessly, the pooled elements will persist across runs in the editor.
And that’s no fun.
Lastly, let’s have a look at the other evils: GenericPool and its twin UnsafeGenericPool.
They are, like their names describe, generic object pools. But there’s something special about them...
So, what’s the deal with these pools of objects?
Again, both GenericPool and UnsafeGenericPool are static object pools. So using them won’t let you disable domain reloads to reduce your editor iteration times.
On the bright side, you don’t have to bother to create them for any of your use cases.
You just use them, whenever, wherever (and whoever) you are.
var pooledGameObject = GenericPool<GameObject>.Get(); pooledGameObject.transform.position = Vector3.one; GenericPool<GameObject>.Release(pooledGameObject);
Just like that.
The UnsafeGenericPool variant performs better at the cost of skipping an important check: the object already-returned check.
You see, when you return an object to the pool, it might be that you already returned it in the past (and didn’t take it out of the pool).
That can be the case easier than you think, especially if you use the static pools and you use the same objects in multiple places
In that case, an item might appear twice in the pool’s internal data structure.
And guess what happens when you take two elements?
BANG!
You’ll be using the same object in different places. You’ll be overwriting each other’s changes.
Imagine you used the same character game object for two different players.
To sum up the differences:
GenericPool uses a static ObjectPool with collectionCheck = true
UnsafeGenericPool uses a static ObjectPool with collectionCheck = false
Ok, so as you saw not everything about pooling is nice and neat.
Let’s rub more salt into the wound.
I could write at least 3 blog posts detailing nasty issues you might experience with pooling.
But instead of doing that, I’ll sum them up here based on an excellent post by Jackson Dunstan.
Here are some of the issues you might face with pooling:
Your objects require an explicit Return.
If you forget to return a pooled object, the garbage collector will have to do the work you wanted to avoid in the first place (in the best case scenario).
You must reset your objects’ state.
If you don’t reset your objects’ state, then old data will leak into the instances you get from the pool.
The object won’t be “fresh” anymore. Your bullets might contain spurs of blood.
If you use the same pooled object in different places, then you must return the object only when all its users are done with it.
You don’t want to return the object multiple times, as that is guaranteed to cause you headaches.
So you require some sort of manual reference counting, expensive CPU checks or a strategy to make sure there is only one owner.
Memory management of collections is hard.
You can pool lists, hashsets, dictionaries and the such. But it’s hard to make assumptions on their sizes.
When you get a list out of a pool, it might come with size 4 while you actually needed a list of size 1000+. You’d force a resize.
It can also happen the other way around.
In short, you might end up wasting a lot of memory in keeping huge collections alive when you only need a few items for them.
Pools are not thread-safe by default.
And if you add mechanisms to support thread-safety, then you’ll be adding CPU overhead that might not pay off anymore.
Some good food for thought there.
Pools are excellent tools to reduce:
The performance cost of game-play allocations
The pressure you put onto the poor garbage collector
And since Unity 2021+ it is now easier than ever to adopt pooling as a developer lifestyle, since we now have a built-in pooling API.
However, I explained the dark side of pooling. A side that might bring you tons of pain during the development of your project.
Pooling is just another performance tool you must know. And the more tools you know, the better.
Do you want to know more tools?
And not only tools, but workflows, optimization tactics and high-performance secrets?
The thing is, you don’t have time to learn the 20% that brings you the 80% your project needs.
That’s why you shouldn’t be on your own in the first place.
Because I know this well, I created the Game Performance Taskforce. An exclusive membership for high-performers where you will get in-depth content every week, every month on the Four Game Performance Pillars:
PRO Performance: become a high-performing developer with better architecture
CPU Performance: destroy your gameplay and draw-calls bottlenecks
GPU Performance: make better graphics cost less, reduce overdraw
MEM Performance: halve your memory usage & loading times, apply addressables
The taskforce includes live lessons where you can ask your questions on the fly.
And before you ask: yes, you'll also join our exclusive Discord community.
If you liked this post, I extend you a risk-free invitation to try the Game Performance Taskforce to stay up-to-date in the industry of high-performing games.
Join The Game Performance Taskforce
Talk soon!
Ruben (The Gamedev Guru)
Read more about:
Featured BlogsYou May Also Like