Why Do I Need Object Pooling?
I think we'll work backwards and talk about why we need it, then talk about what it is...
Let's think of a scenario where you have a shooting game. Each bullet renders on the screen as a GameObject and the player is pretty trigger happy, letting loose four bullets a second for a duration of a three minutes.
Over this time, the player has instantiated 720 game objects!
Now I know what you may be thinking... Surely you destroy the bullet when it has reached it's lifetime so that won't be a problem - there won't be 720 bullets in the scene.
You're right, of course you would do something about the stray bullets - however when it comes to memory management cleanup (see ref 1) and the Garbage Collector, there is going to be some performance cost which can introduce the dreaded micro-stutter!
I have performed a very basic test to prove the theory by replicating the above scenario over 20 seconds of shooting 4 projectiles a second. The scene was very basic and the projectiles were nothing more than a standard Unity cube. Let's take a look at the asset memory consumption gathered from the profiler shall we!
Without Object Pooling: 402KB
With Object Pooling: 146KB
"Whoa... Steady on cowboy, I'm pretty sure my system can handle a spare 256KB of memory" I hear you say.
True... However, keep in mind the situation...
- There is nothing in this scene except basic cube geometry
- The tests were run for only 20 seconds
This is a 36% increase in memory efficiency!
This is exactly why we need object pooling - performance!
So what is it?
What is Object Pooling
In it's simplest definition, object pooling can be thought of as recycling.
Rather than instantiating a whole bunch of clone GameObjects only to destroy them shortly after, Object Pooling simply disables the object and tucks it away for reuse later on when a new object of that type is needed.
There are likely a stupendous amount of examples for how to create an Object Pooling system - so let us add to the list and I'll demonstrate my implementation below ;)
Creating An Object Pooling System
Let's build an Object Pooling System!
What do we need? Well let's see, we need to...
- Set a starting pool size
- Know what object to pool
- Handle dishing out the objects in the pool
- Handle what happens if the pool is empty
- Know when to recycle the object (normally when it would be destroyed)
Cool - so let's do ourselves a favour and make sure we can easily listen out for the recycle request - we should use an event! I've opted to create a MonoBehaviour class which will be added to any recyclable object...
// IPoolable.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class Recyclable : MonoBehaviour
{
public event Action RecycleEvent;
private void OnRecycleEvent(GameObject obj)
{
Action handler = RecycleEvent;
if (handler != null)
{
handler(obj);
}
}
public void Recycle()
{
OnRecycleEvent(gameObject);
}
}
This class can be added to any object now via an Object Pool Manager. This manager's responsibilities will be to add this component and listen out for the event when it has called to be recycled.
Below is the entire pool class which comments really speak for themselves, but I'll point out some important notes below...
// ProjectilePool.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class ObjectPool
{
private GameObject objectPrefab;
private List activePool = new List();
private List readyPool = new List();
private const int POOL_SIZE = 5;
private Transform objectParent;
public ObjectPool(GameObject poolableObject, Transform parent)
{
BuildPool(poolableObject, parent);
}
public void BuildPool(GameObject poolableObject, Transform parent)
{
objectParent = parent;
objectPrefab = poolableObject;
for (int i = 0; i < POOL_SIZE; i++)
{
// Instansiate a bunch of objects ready to go
GameObject obj = GameObject.Instantiate(objectPrefab, objectParent);
obj.SetActive(false);
// Check if Recyclable component has been added
if (obj.GetComponent() == null)
obj.AddComponent();
readyPool.Add(obj);
}
}
public GameObject GetFromPool()
{
// If there are no ready objects, spawn a new one and add to the list
if (readyPool.Count == 0)
{
// Run out of objects - spawn new one
GameObject obj = GameObject.Instantiate(
objectPrefab, objectParent);
obj.SetActive(false);
obj.AddComponent();
// Subscribe to Recycle Request event
obj.SetActive(true);
obj.GetComponent().RecycleEvent += Recycle;
return obj;
}
else
{
// Pull out first objects from ready list and move into active list
GameObject obj = readyPool[0];
readyPool.Remove(obj);
activePool.Add(obj);
obj.SetActive(true);
// Subscribe to Recycle Request event
obj.GetComponent().RecycleEvent += Recycle;
return obj;
}
}
private void Recycle(GameObject obj)
{
// unsubscribe from Recycle Request event
obj.GetComponent().RecycleEvent -= Recycle;
// Disable GO ready to be used again, move to ready list
obj.SetActive(false);
activePool.Remove(obj);
readyPool.Add(obj);
}
public void PurgePool()
{
if (activePool.Count > 0)
for (int i = 0; i < activePool.Count; i++)
{
PurgeGO(activePool[i]);
}
if (readyPool.Count > 0)
for (int i = 0; i < readyPool.Count; i++)
{
PurgeGO(readyPool[i]);
}
}
private void PurgeGO(GameObject go)
{
activePool.Remove(go);
GameObject.Destroy(go);
}
public bool CheckPrefabMatch(GameObject match)
{
return (match == objectPrefab) ? true : false;
}
}
What a mouth full ;)
Let me explain what is happening here.
The Constructor
The constructor is taking in two parameters, a prefab and a parent location. Pretty straight forward, the constructor loops through a bunch of times and fills up a list of GameObjects ready to be used, also making sure they are set to be disabled.
GetFromPool
This function returns a GameObject from... You guessed it, the object pool!
First it checks if there are any free from the ready list, if not - it will instantiate a new one, growing the list dynamically as required.
Importantly - there is something noteworthy happening here...
It is subscribing the method Recycle to the RecycleEvent (see ref 2 for more info on events).
Also, note that if there isn't any objects available, the pool simply expands by one :)
Recycle
As it states, this nifty little method does the object recycling. It will unsubscribe from the object's recycle event, set it inactive and move it from the active pool into the inactive pool.
Nice and simple.
How To Implement It
This is nice and simple! For this example I created an Object Pool Manager as a singleton to look after all recyclable objects in the scene..
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectPoolManager : MonoBehaviour
{
public static ObjectPoolManager instance;
public List projectilePool = new List();
private void Awake()
{
if (instance == null)
instance = this;
else
Destroy(this);
}
public void BuildPool(GameObject prefab, PoolType type)
{
// Check if pool exists
if(!CheckPoolExists(prefab, type))
{
List poolList = GetPool(type);
ObjectPool newPool = new ObjectPool(prefab, transform);
poolList.Add(newPool);
} // else never mind because this projectile already exists
}
private bool CheckPoolExists(GameObject prefab, PoolType type)
{
bool result = false;
List poolList = GetPool(type);
for (int i = 0; i < poolList.Count; i++)
{
if(poolList[i].CheckPrefabMatch(prefab))
{
// match found
return true;
}
}
return result;
}
public GameObject GetObject(GameObject objectPrefab, PoolType type)
{
// find which pool
List poolList = GetPool(type);
for(int i = 0; i < poolList.Count; i++)
{
if (poolList[i].CheckPrefabMatch(objectPrefab))
{
return poolList[i].GetFromPool();
}
}
Debug.LogError("Object not found in pool");
return null;
}
private List GetPool(PoolType type)
{
switch (type)
{
case PoolType.projectile:
return projectilePool;
case PoolType.tileset:
return tilesetPool;
default:
Debug.LogError("Pool not found!");
return null;
}
}
}
This script does most of the heavy lifting :)
The Object Pools can be referenced from other classes, such as a weapon system and pull a copy from the pool. But wait... Is that a LIST of pools :o
Yes! So you can see how powerful this manager can be.
This manager checks for a matching prefab type, if one is found then it returns the next available object from it's respective pool!
Conclusion
As mentioned, there are numerous ways to handle Object Pooling - this is just one. You could also build on this - for example my weapon had two modes of fire, so all I had to do was request the projectile prefab, and bam! The next available one was returned for duty.
If anything, you should now have a solid concept of what an Object Pool is and why you should use them!
References