Easy Pooling in Unity

Seona Bellamy
5 min readJun 3, 2021

--

Let’s say your game is going to send waves of enemies against the player. A wave may have 30 or 40 enemies in it, but assuming the player is actually going to be fighting back there may never be more than 5–10 enemies on screen at any given time. They die as the player shoots them, and are quickly replaced by others. The easiest way to do this is, of course, to instantiate a copy of the enemy (you did prefab them, didn’t you?) just before they appear, and then destroy that copy when the player kills it.

The trouble with this is that it’s a very resource-intensive process. And if you have a lot of it going on very rapidly, then your game can start to lag. A non-optimal situation for your player, and a non-optimal situation for you when they leave an unflattering review!

Pooling to the rescue!

At the start of the game you’ll create a set of enemy objects and then disable them so that they don’t appear on screen or do any of their usual processing. Then, when the game needs to display an enemy, you find the first inactive enemy in the list and activate them. They’ll behave just as they would if you’d instantiated them fresh, and when they die you just deactivate them again instead of destroying them. Then they’re ready to be used again.

Setting up the PoolManager

Create en empty object in your hierarchy called PoolManager, then create and attach a PoolManager script. Open it in your editor of choice. Make the script a MonoSingleton. At the top of the class, add the following variables:

[SerializeField]
private int _defaultPoolSize = 10;
[SerializeField]
private GameObject _enemyPrefab;
private List<GameObject> _enemyPool = new List<GameObject>();

First of all we’re defining a default size for our pool — how many objects we want to populate it to start with. Then we create a holder for the enemy prefab, and finally a list which will hold all of our pooled prefabs. Note that we’re also serialising the first two variables so that we can interact with them in Unity’s inspector. So before you forget, pop back into Unity and drag your enemy prefab into the field on the PoolManager to set up that link.

Populating the Pool

The first thing we need to do is to ensure that the pool gets filled with enemies when the game starts.

void Start()
{
GenerateEnemies(_defaultPoolSize);
}

List<GameObject> GenerateEnemies(int amount)
{
for (int i = 0; i < amount; i++)
{
GameObject enemy = Instantiate(_enemyPrefab);
enemy.SetActive(false);

_enemyPool.Add(enemy);
}

return _enemyPool;
}

In Start() we’re simply calling on a GenerateEnemies() method and passing it the default pool size. The reason I do it this way despite the fact that I could just call _defaultPoolSize directly where I use it is because it provides me with an easy place to modify the value. Mostly in a project you will have multiple pools — one for enemies, one for bullets, one for powerups, etc. Sometimes it might make sense for, say, the bullet pool to start twice as large because there will frequently be a large number of bullets on screen at once. It’s a lot easier to see what’s happening if you have

void Start()
{
GenerateEnemies(_defaultPoolSize);
GenerateBullets(_defaultPoolSize * 2);
}

If you bury that modification in the GenerateBullets() method, it takes longer to find where that value is being modified.

Anyway, let’s look at that method now. We loop through based on the desired pool size we passed in and on each loop we instantiate the enemy prefab and immediately set it to inactive. Then we add it to our pool list. Finally, after we’re created as many enemies as we wanted, we return the list. This isn’t strictly speaking necessary — this could just as easily be a void method — but I find it’s useful to make it return the list for use in debugging. Making your life easier as a developer is never a bad thing!

Drawing from the Pool

When we want to use an enemy in the scene, we’ll be asking the PoolManager to give us one. This is done with the following method:

public GameObject RequestEnemy()
{
foreach (GameObject enemy in _enemyPool)
{
if (enemy.activeInHierarchy == false)
{
enemy.SetActive(true);
return enemy;
}
}

GameObject newEnemy = Instantiate(_enemyPrefab);
_enemyPool.Add(newEnemy);

return newEnemy;
}

First of all we loop through our pool list to find an enemy that is currently inactive. When we find one, we make it active and return it. Pretty simple.

The second part of this method is essentially proofing against overruns. If we get through the whole list without finding an inactive enemy, that means that every enemy in the pool is currently in use. Rather than throw an error or just fail to return an enemy at all, we instantiate a new one and add it to the pool. Because this is not happening all the time, the performance hit is negligible. Plus we’re still not destroying them. In fact, we’ve just made the pool bigger to reduce the likelihood of having to do this again. And, if the player is struggling and the enemies are building up on screen for a little while, well the pool will just keep growing while they get their act together (or die).

But how do we actually use it, you ask?

In your GameManager or SpawnManager or wherever else you would normally be instantiating your enemy prefab, you’ll replace that instantiation call with the following:

GameObject enemy = PoolManager.Instance.RequestEnemy();
enemy.transform.position = new Vector3(_xPos, _yPos, 0);

We call the PoolManager’s instance and request and enemy, which we then position where we want — remember, by default it will activate in its last location if we don’t change that, and we probably want our enemies coming in from the sides rather than popping out of thin air in the middle of the battlefield. Although you could do that too I guess.

If you enemy has any variables such as health or animation state that should be reset, you’ll do that in the Enemy script itself in the OnEnable() method. This ensures it will be run every time the PoolManager activates it.

“Killing” a Pooled enemy

Back in the PoolManager script, we need one more method:

public void DespawnEnemy(GameObject enemy)
{
enemy.SetActive(false);
}

This one does what it says on the tin — it sets the enemy to inactive.

To use this, in the Enemy script we go to wherever we would normally be destroying the instance and replace that destruction with the following line:

PoolManager.Instance.DespawnEnemy(gameObject);

If you want something to happen before the enemy pops out of existence, such as a death animation or a particle effect or even just a sound, then you can put this inside a coroutine to delay the actual despawn call until all of your fancy death effects have finished.

--

--