Object Pool Design Pattern In Unity

Published on Jan 28, 20247 min read

The object pool design pattern is a creational design pattern that deals with the creation of an object. It is used to manage a pool of objects to be reused when the cost of initializing a new instance of an object is high or where object creation and destruction are frequent and resource-intensive.


In the case of game programming using Unity, GameObject creation can be an expensive operation in terms of time and resources, so instead of creating and destroying objects frequently, you can use the Object Pool pattern to recycle and reuse them. This can be vastly used in game development scenarios where a large number of similar objects are need to be spawned such as projectiles or enemies.

Case study

Image alt



Here, I'm having a simple project setup where the user can turn a turret and shoot projectiles. The code implementation instantiates a new projectile object when the user presses the left mouse button and the projectile is set to destroy in three seconds after instantiation.

Cannon Script

csharp

1...
2
3  // Transform at which projectile is shot
4  [SerializeField] private Transform firePoint;
5  // Firing rate of projectiles
6  [SerializeField] private float fireRate = 0.2f;
7  // Reference to the projectile prefab
8  [SerializeField] private Projectile prefab;
9
10  // Update is called once per frame
11  private void Update()
12  {
13      // If LMB press and hold
14      if (Input.GetMouseButton(0) && Time.time >= mNextFireTime)
15      { Shoot();  }
16  }
17
18  // Handles a single projectile shooting
19  private void Shoot()
20  {
21      // Offset timer for next firing
22      mNextFireTime = Time.time + 1f / fireRate;
23
24      // Instantiate the projectile prefab (as Projectile)
25      var projectile = Instantiate(prefab);
26      // Fire the projectile
27      projectile.Fire(firePoint);
28  }
29
30...
31

Projectile Script

csharp

1public class Projectile : MonoBehaviour
2{
3  // Launch velocity of this projectile
4  private float mVelocity = 15.0f;
5  // Reference to the Rigidbody
6  private Rigidbody mRigidbody;
7
8  // Unity Awake
9  private void Awake()
10  {
11    // Cache reference to Rigidbody component
12    mRigidbody = GetComponent<Rigidbody>();
13  }
14
15  // Fires this projectile from the provided transform point
16  public void Fire(Transform firePoint)
17  {
18    // Set the projectile position to the turret's firing point
19    transform.position = firePoint.position;
20    // Set the firing velocity when the projectile is activated
21    mRigidbody.velocity = firePoint.forward * mVelocity;
22
23    // Set to destroy this object after 3 seconds
24    Destroy(this.gameObject, 3.0f);
25  }
26}
27

As you see here, the current approach is functional but when it comes to performance on a large scale, this can become inefficient and create performance impacts. To implement object pooling, our current approach is going to be changed so that we ensure that the objects are reused. Object pools often have a fixed size or a maximum number of objects. If all objects in the pool are currently in use, the application might need to handle this scenario, either by waiting for an object to become available or by creating a new one. In our implementation, we will go with the later approach.

Psuedo Logic

The below flow-diagram illustrates the setup we are going to implement.

This way, the objects are getting reused rather than destroyed and new object instances are only created when pool cannot supply the demand.


Image alt

PoolManager Class

Inorder to implement the object pool design pattern, let's start by creating the manager script for the pool as PoolManager.cs. The PoolManager handles the instantiation of the creation of initial set of projectile instances and the dynamic creation of instances on demand. The class/component responsible for shooting the projectile can request the PoolManager to provide a projectile instance.

csharp

1public class PoolManager : MonoBehaviour
2{
3    // Startup size of the projectile pool
4    [SerializeField] private int poolStartSize = 10;
5
6    // List to track the projectile objects
7    private readonly List<Projectile> mProjectilePool = new();
8
9    // Use this for initialization
10    private void Start()
11    {
12      // Create the initial pool set
13      for (int i = 0; i < poolStartSize; i++)
14      { CreateProjectile(); }
15    }
16
17    // Creates a single Projectile instance, add to the pool and return the object reference
18    private Projectile CreateProjectile()
19    {
20        // Instantiate a new projectile object
21        var projectile = Instantiate(projectilePrefab).GetComponent<Projectile>();
22        // Add as a child to this gameObject
23        projectile.transform.parent = this.transform;
24        // Keep the projectile deactivated
25        projectile.gameObject.SetActive(false);
26        // Add this projectile to the pool list
27        mProjectilePool.Add(projectile);
28        // Return the projectile object
29        return projectile;
30    }
31
32    // Gets the reference to an inactive/non-using projectile object from the pool
33    public Projectile GetProjectile()
34    {
35        // Loop through each object in the pool list
36        foreach (Projectile projectile in mProjectilePool)
37        {
38            // If this object is not active, return it
39            if (!projectile.gameObject.activeSelf)
40                return projectile;
41        }
42
43        // If no projectile is found free in the pool, create a new one and return the reference
44        return CreateProjectile();
45    }
46}
47

If you take a look at the above PoolManager class, instances of the projectile with a count of 10 (poolStartSize) gets created on initialization and the references to these instances are kept in mProjectilePool List object. Now the Cannon script can call the GetProjectile() method to get the reference to an available projectile in the pool. Here's how the modified Cannon script looks.

Cannon Script (Updated)

csharp

1...
2
3// Reference to the PoolManager
4[SerializeField] private PoolManager poolManager;
5
6// Handles a single projectile shooting
7  private void Shoot()
8  {
9      // Offset timer for next firing
10      mNextFireTime = Time.time + 1f / fireRate;
11
12      // Get an available projectile from the projectile pool
13      var projectile = poolManager.GetProjectile();
14      // Set the projectile object to active
15      projectile.gameObject.SetActive(true);
16      // Fire the projectile
17      projectile.Fire(firePoint);
18  }
19
20...
21

The Projectile class also needs some modifications so that it can get deactivated rather than getting destroyed and can be reused by the PoolManager for the next cycle.

Projectile Script (Updated)

csharp

1public class Projectile : MonoBehaviour
2{
3  // Launch velocity of this projectile
4  private float mVelocity = 15.0f;
5  // Reference to the Rigidbody
6  private Rigidbody mRigidbody;
7
8  // Unity Awake
9  private void Awake()
10  {
11    // Cache reference to Rigidbody component
12    mRigidbody = GetComponent<Rigidbody>();
13  }
14
15  // Fires this projectile from the provided transform point
16  public void Fire(Transform firePoint)
17  {
18    // Set the projectile position to the turret's firing point
19    transform.position = firePoint.position;
20    // Set the firing velocity
21    mRigidbody.velocity = firePoint.forward * mVelocity;
22
23    // Deactivate this projectile in 3 seconds rather than destroying it
24    Invoke(nameof(Deactivate), 3.0f);
25  }
26
27  // Also you may implement logic to deactivate on collision with enemy or boundaries
28  private void OnCollisionEnter(Collision other)
29  {
30      ...
31  }
32
33  // Deactivates this projectile to reuse for next cycle
34  private void Deactivate()
35  {
36    // Reset velocity
37    mRigidbody.velocity = Vector3.zero;
38    // Disable the projectile object
39    gameObject.SetActive(false);
40  }
41}
42

If you take a look at the below screenshots,


Image alt


Object pools can be customized to handle different scenarios, such as pre-warming, resizing, or handling object lifetime strategies. Though the above setup is rudimentary, the fundementals of this design pattern is the same and one could add improvements.


Wishing you success on your learning journey...✌

Copyright © 2024 rendercodeninja. All rights reserved.

Made with ❤ by rendercodeninja.GitHub