Skip to content

Object Pooling

Luke Mirman edited this page Sep 16, 2022 · 4 revisions

Introduction

Object Pooling is a optimization system which will allows for the recycling of game objects without having to destroy and instantiate them. This is particularly useful for games which have a lot of quickly created and destroyed objects such as 'bullet hell' games that have significant amount of projectiles.

Design Overview

The design of the system is the followingL

  • Poolable - A component that must be attached to a game object you wish to keep in the Object Pool system.
  • ObjectPool - A static class which manages all Poolable game objects which are created using it's own but identical Instantiate command. Do not use the standard game object's Instantiate command for Poolable game objects.

The Object Pool Lifecycle

When the object is first instantiated it is Populated into the pool. This will only occur once when it is initially created. After it is populated it is then Retrieved from the pool into the active scene. At any point, according to your own application logic, the object will be Returned to the pool when it is done being used, where it will be inactive. The next time an poolable prefab is instantiated it will prioritize Retrieving an item from the pool rather than creating a new one.

If a poolable item is ever properly destroyed it will be Returned to the pool if currently active and then Disposed of.

graph TD;
	Population-->Pool
	Pool-->Retrieve
	Return-->Pool
	Pool-->Dispose
Loading

Practicality of the Lifecycle

Given this lifecycle you can consider these states as practically:

  • Populate - The game object has been created for the very first time. Generic initialization such as GetComponent and likewise can be included here.
  • Retrieve - The game object has been 'instantiated'. This can occur any number of times and logic here should initialize the object for this particular iteration of it such as resetting timers or such values.
  • Return - The game object is done being used in the scene. This should clean up any state that shouldn't carry over into the next iteration of the object.
  • Dispose - The game object is being destroyed, never to be used again. If you subscribed to any events in Populate this is a good place to unsubscribe.

Example - Projectile Pooling

For this example we are going to create a system that will pool projectiles in a shooter game.

Disclaimer: This example is primarily focused on specifically the object pool system not a practical shooter implementation.

Making the Projectile Prefab

First let's create a prefab for our games projectile. For this example it will just be a stationary cube in the game world.

  1. Create the cube by right clicking in the Hierarchy and choosing Create -> 3D Object -> Cube
  2. Name the Cube "Poolable Projectile"
  3. Drag the game object into the Project view to make it a prefab.
  4. Delete the instance of the prefab in the scene.
  5. With the "Poolable Projectile" open in the Prefab Editor add the Poolable component to the projectile.
    • This is required and enables our prefab to work within the ObjectPool system.
    • Remark: You may notice the Pool Capacity Limit Behavior field. This allows you to optionally prevent the pool from getting to large through various methods. These behaviors will be covered in a later section.
  6. Add a Rigidbody to the component for moving it later.
  7. Create a custom component Projectile and add it to the prefab.

Scripting Our Poolable Projectile

Now that we have our prefab setup we need to write the code that will actually handle the projectile itself. Once again this script is mainly focused on the poolable system itself not an actual working projectile.

Sample Projectile Code

using LMirman.Utilities;
using UnityEngine;
using Random = UnityEngine.Random;

[RequireComponent(typeof(Poolable))]
public class Projectile : MonoBehaviour
{
	private Rigidbody rb;
	private Poolable poolable;
	private float destroyTimer;

	private void Awake()
	{
		poolable = GetComponent<Poolable>();
		rb = GetComponent<Rigidbody>();
		poolable.ObjectRetrieved += PoolableOnObjectRetrieved;
	}

	private void FixedUpdate()
	{
		if (poolable.IsActive)
		{
			destroyTimer -= Time.deltaTime;

			if (destroyTimer < 0)
			{
				poolable.Return();
			}
		}
	}

	private void OnCollisionEnter(Collision collision)
	{
		poolable.Return();
	}

	private void OnTriggerEnter(Collider other)
	{
		poolable.Return();
	}

	private void PoolableOnObjectRetrieved()
	{
		destroyTimer = 2.5f;
		rb.velocity = Vector3.up * Random.Range(1f, 10f);
		rb.angularVelocity = Vector3.zero;
	}
}

Scripting Our Projectile Emitter

The last thing we need to do now is create an emitter for our projectiles. In the scene create a new empty game object and add a custom component ProjectileEmitter to it.

This emitter will use the ObjectPool.Instantiate function when the user left clicks to recycle inactive objects into the scene or create a new one if there are none waiting.

Sample Projectile Emitter Code

using LMirman.Utilities;
using UnityEngine;

public class ProjectileEmitter : MonoBehaviour
{
	public GameObject projectileTemplate;
	
	private Camera mainCamera;
	
	private void Awake()
	{
		mainCamera = Camera.main;
	}

	public void Update()
	{
		if (Input.GetMouseButtonDown(0))
		{
			Vector3 position = mainCamera.ScreenToWorldPoint(Input.mousePosition);
			position.z = 0;
			ObjectPool.Instantiate(projectileTemplate, position, Quaternion.identity);
		}
		else if (Input.GetMouseButtonDown(1))
		{
			ObjectPool.DisposeAllIdlePools();
		}
	}
}

Testing the Object Pool

With the Poolable Projectile prefab assigned to the Emitter projectileTemplate enter play mode and left click in the game window. Looking in the Hierarchy you can see the projectiles being created and reused.

Pool Limit Behaviors

Sometimes you want to prevent the Pool of a certain type of object from getting to large. In order to do so each Poolable has a Limit Capacity Limit Behavior which defines what it should do when the pool has reached a certain capacity. The behaviors are as follows:

  • None - Will never automatically manage the capacity of the pool.
  • Dispose on Return - After an object is returned: if the inactive object pool is too large it will then be immediately disposed.
  • Retrieve Oldest Active - When the number of active/inactive objects is reached: instantiating a new object will immediately retrieve the oldest active object without returning it to the pool.
  • Recycle Oldest Active - When the number of active/inactive objects is reached: instantiating a new object will immediately return it to the pool and then retrieve it.
  • Reject Population - When the pool is at capacity, do nothing and return null to the instantiation request.
  • Throw Exception - When the pool is at capacity, throw an exception on instantiation request.

Clone this wiki locally