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.
Design patterns are proven solutions to recurring design problems. They provide templates or blueprints for structuring your code to achieve certain objectives, such as making code more maintainable, scalable or to improve performance.
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.
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.
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
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.
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.
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.
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.
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,
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.