Advanced Object Pooling in C# and Unity

Thomas Steffen
4 min readMay 8, 2023

--

Object pooling is a technique used in game development to optimize performance and reduce memory allocation. It involves creating a pool of reusable objects and keeping them in memory instead of constantly creating and destroying new instances of those objects.

In Unity, object pooling involves creating a pool of GameObjects that are disabled and stored in memory when not in use. When an object is needed, instead of creating a new instance of it, an existing object from the pool is activated and moved to the desired location.

Object pooling is useful for objects that are frequently created and destroyed during gameplay, such as projectiles, enemies, and particles. By reusing existing objects, object pooling reduces the overhead of creating and destroying new objects, which can result in better performance and smoother gameplay.

Implementing object pooling in Unity typically involves creating a script to manage the pool of objects and instantiate them at the start of the game or on-demand, as needed. The script also handles activating and deactivating objects in the pool as they are used and returned.

Today, I want to dive into a real example I created that utilizes multiple prefabs to create a simultaneous object pools.

I wrote a simple script that you place on the object you want “pooled”. I called this method instead of using Destroy(this.gameObject) on my bullets. They will be disabled and recycled instead of destroyed.

public class PooledObject : MonoBehaviour
{
public void ReturnToPool()
{
gameObject.SetActive(false);
}
}

Next, I added this into my GameManager, but you can place it in any manageable game object in your hierarchy. For example, “ObjectPool_Manager”, etc.

In this example, I get a little crazy and create an Array of Lists. It can become a little mind-bending to keep track of nested arrays/lists, especially when creating loops involving them. It is important to note that in the PooledObject method, the first element [i] reference is referencing the array element / which list we want. The second element, accompanied by the first, [i][j] is referencing the object within the selected List.

It may help to think of it in terms of Rows and Columns.

public class ObjectPool : MonoBehaviour
{
public PooledObject[] _prefab;
public int[] _poolSizes;
public List<PooledObject>[] _pools;

void Start()
{
_pools = new List<PooledObject>[_prefab.Length];

//Define each list. Set the initial length of each list by adding in the available PooledObject prefabs.
///Each list should have it's own prefab that populates it, they will not be mixed together
for (int j = 0; j < _pools.Length; j++)
{
_pools[j] = new List<PooledObject>();
for (int i = 0; i < _poolSizes[j]; i++)
{
PooledObject obj = Instantiate(_prefab[j]) as PooledObject;
obj.gameObject.SetActive(false);
_pools[j].Add(obj);
}
}
}


//array pooledobject
public PooledObject Get(PooledObject _projectile)
{
for (int i = 0; i < _prefab.Length; i++)
{
//In the script that calls this method, we check to match if that enemy's projectile matches the projectile in our pool. If so, we activate one, it is moved in the fire script.
if (_prefab[i] == _projectile)
{
//check to see the length of each list - to be removed after debug.
Debug.Log("This pool is list " + i + ". This pool's size = " + _pools[i].Count);
for (int j = 0; j < _pools[i].Count; j++)
{
if (_pools[i][j].gameObject.activeInHierarchy == false)
{
_pools[i][j].gameObject.SetActive(true);
return _pools[i][j];
}
//if we're at the end of the list's count/length, then we need to increase the pool and use an additional object
if ((j == (_pools[i].Count - 1)) && (_pools[i][j].gameObject.activeInHierarchy == true))
{
IncreasePool(i);

return _pools[i][j+1];
}
}

}
}
return null;
}

//Increase the pool by instantiating another object correctly coorelating to the specified List, make sure it's active, and add it to the correct List.
private void IncreasePool(int _specifiedPool)
{
PooledObject obj = Instantiate(_prefab[_specifiedPool]) as PooledObject;
obj.gameObject.SetActive(true);
_pools[_specifiedPool].Add(obj);
}

Note: The line — “return _pools[i][j+1];” uses +1 to signify the added object to the pool. Without the +1 it would be returning the previous object already in the pool, which would result in some strange behavior.

Next, we need to actually Get the objects into the scene and then call a method to deactivate them when they’re no longer needed.

Without pasting in a lengthy enemy AI script, I’ll post the relevant information to this object pool:

    //The object pool attached to my GameManager
public ObjectPool pool;

//The projectile we plan to Match to the pool to define which prefab to Get
[SerializeField]
private PooledObject _projectile;



private void EnemyFire()
{
//cooldown timer
if (timer >= _firerateCooldown)
{
//fire from multiple firepoints
for (int i = 0; i < _firepoints.Length; i++)
{
///THESE TWO LINES OF CODE TELLS OUR OBJECT POOL TO GET AN OBJECT,
///ACTIVATE IT, AND MOVE IT IN PLACE
PooledObject obj = pool.Get(_projectile);
obj.transform.position = _firepoints[i].transform.position;
}
//set audio clip and play audio when firing
_audio.clip = _fireSound;
_audio.Play();
//reset timer
timer = 0;
}
}

Finally, we need the objects to become disabled when they are no longer needed. In this example, I disable the game objects after 2.5 seconds or on impact with various tagged game objects:

    //THIS FUNCTION IS CALLED WHEN AN OBJECT BECOMES ENABLED AND ACTIVE
void OnEnable ()
{
StartCoroutine(ReturnToPool(2.5f));
}


public void OnCollisionEnter(Collision collision)
{
if(collision.collider.tag == "PlayerHitArea")
{
PlayerController _player = collision.collider.gameObject.GetComponent<PlayerController>();
_player.TakeDamage(damage);
///INSTEAD OF DESTROYING THIS OBJECT, WE DISABLE IT AND RECYCLE IT BACK TO THE POOL
StartCoroutine(ReturnToPool(0));
//Destroy(this.gameObject);
}
}

public IEnumerator ReturnToPool(float _seconds)
{
yield return new WaitForSeconds(_seconds);
_pooledObject.ReturnToPool();
Debug.Log("Deactivate object " + transform.parent.name);
//gameObject.SetActive(false);
}

Sometimes I would use OnTriggerEnter when colliding my bullets to avoid some physics affects, but due to the specifics of this project, I’m using OnCollisionEnter. Either method will work. The main point is making sure you are not destroying your objects, but disabling them so that your object pool can call them back when needed.

To further help with organization, you can initially instantiate your objects as a child under a more organized naming convention as well as when you add more objects to the pool.

--

--

Thomas Steffen
Thomas Steffen

Written by Thomas Steffen

I am Virtual Reality Developer, UI Systems, and general programmer with a passion for Unity software development.

No responses yet