Skip to content

Object Pooling

Luke Mirman edited this page Feb 13, 2023 · 4 revisions

Introduction

Object Pooling is an optimization system which supports the recycling of game objects without destroying and instantiating them. This can optimize performance in certain scenarios because the game does not have to clean up as much behind the scenes when game objects are destroyed.

Design Overview

The design of the system is the following:

  • 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 Instantiate method. This commands is mostly identical to the regular Instantiate method regarding parameters. You are required to use ObjectPool.Instantiate when creating pooled game objects for them to be managed by the ObjectPool system.

The Object Pool Lifecycle

  1. When ObjectPool.Instantiate is invoked it will search the current object pool for any inactive objects to reuse. If none are present it will Populate a new game object into the pool by traditionally instantiating it.
    1. Population only occurs once for a particular game object throughout its lifecycle occurring when it is first created.
  2. When a game object from the pool enters the active scene it is Retrieved from the pool.
    1. This occurs each time the object is reused. It is ensured to be invoked every single time the object is "instantiated" by ObjectPool.Instantiate
  3. At some point, dependent on your own application logic, the object will be Returned to the pool when it is done being used, where it will be inactive until reused or disposed of.
    1. The next time ObjectPool.Instantiate is used it will Retrieve an inactive item from the pool if one is present rather than creating a new one.
  4. If a pooled object is destroyed by any means including the native Destroy method, the scene unloading, or flushing the inactive object pool: the poolable will be Disposed removing it from the object pool system. This occurs once at the very end of the object's life cycle.
    1. If a poolable object is still active it will first be Returned to the inactive pool before being immediately disposed of.
stateDiagram-v2
  Active: Active Scene
  Inactive: Inactive Object Pool
  [*] --> Inactive: Populate 
  Inactive --> Active: Retrieve
  Active --> Inactive: Return
  Inactive --> [*]: Dispose
Loading

Practicality of the Lifecycle

Given this lifecycle you can consider these states practically as:

  • 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 as the object is reused. Thus, logic here should initialize the object for this particular iteration of it such as resetting timers or setting unique 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 or inform other behaviors that this object should no longer be used.
  • 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. This may occur in the OnDestroy call stack so avoid doing anything in here you wouldn't also do in OnDestroy otherwise.

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.

When to Use Object Pool

  • You are going to be frequently instantiating the same template game object repeatedly such as:
    • A bullet hell game where you are constantly creating a destroying projectiles
    • Particle effects or visual effects that occur repeatedly as one shots such as splashes, explosions, or sparkles.

When Not to Use Object Pool

  • You don't expect an object to ever go through the Retrieve -> Return cycle at least 3 times.
  • References to your game object are sensitive and can't know when an object has been returned.
    • This is to say that a pooled object will have the same unity instance ID throughout its entire life and therefore other non-local behaviors that reference a component or the game object itself need to be conscious of the state of the pooled object.
  • The added complexity of working with the object pool is simply not worth it.

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.