Object Pooling: An Essential Game Development Practice
Object Pooling 101
Object pooling is a design pattern used in computer programming to save on the computational expense of creating and destroying objects. This practice is particularly beneficial in a game development environment such as Unity, where performance is critical for maintaining a seamless gameplay experience.
At its core, Object Pooling is a design pattern that uses a set of initialized objects, or an "Object Pool", kept ready for use rather than instantiating and destroying objects on the fly. Once an object is taken from the pool, it's not available in the pool until it's returned. Objects in the pool have a lifecycle: inactive while waiting in the pool, and active when they are being used.
For my game, this concept is illustrated in the above diagram. The PoolManager acts as the overseer of multiple ObjectPools, each containing a specific type of object: Bullets, Lasers, Fighters, and Missiles in this example. The shading in the diagram represents the active (dark shade) and inactive (light shade) states of the objects in each pool.
Why Object Pooling?
I’ve chosen to use object pooling for the following key reasons:
A Known Problem: Unity's Instantiate() and Destroy() methods can be quite expensive, especially when dealing with a large number of game objects that are very frequently created and destroyed, such as projectiles or particle effects. Constant use of these expensive operations can lead to lowered or inconsistent performance and frame rates, negatively impacting gameplay.
Low-Hanging Fruit: By implementing object pooling, we trade instantiation and destruction for activation and deactivation of game objects, which is computationally much cheaper. In game development, it's essential to save performance where it can reasonably and efficiently be saved. As a known best-practice, object pooling avoids the trap of premature optimization and yields demonstrable benefits on projects of almost any size.
A Good Trade: While we still have to instantiate objects at some point (usually as the game is loading), front-loading the majority of these operations leads to a lowered, more consistent demand on resources, improving performance and providing smoother gameplay. An extra half-second of loading screen is significantly more palatable to the average gamer than choppy or inconsistent framerates. I’d say that’s a good trade.
Object Pooling in Danger Space
In Danger Space, object pooling is used in the management of various game objects that would otherwise be frequently instantiated and destroyed: particles, pickups, projectiles, ships, and trails. This use of object pooling achieves a small overall increase to the average framerate and significantly improves its consistency.
Below is an overview of how object pooling is implemented in Danger Space:
PoolManager
The PoolManager
class is the heart of the object pooling system in Danger Space. It is responsible for the creation, initial population of, and access to ObjectPools
for the different types of prefabs used in the game. The initial creation and population of the ObjectPools
is done in the PoolManager
‘s Awake()
method.
The PoolManager
maintains a dictionary of ObjectPools
, keyed by the prefab they are associated with. This design allows quick and efficient access whenever an instance of a specific prefab is needed.
Github Code Section
gh-repo="BIRD-COMMAND/danger-space-public"
gh-branch="feature/portfolio-polish"
gh-file="Assets/Scripts/Pooling/PoolManager.cs"
gh-start="70" gh-end="84"
ObjectPool
The ObjectPool
class is a generic object pool for Poolable
objects. It contains a queue to store the available Poolable
objects. When an object is needed, it's dequeued from the pool. If there are no available objects, a new one is instantiated, initialized, and added to the pool.
Github Code Section
gh-repo="BIRD-COMMAND/danger-space-public"
gh-branch="feature/portfolio-polish"
gh-file="Assets/Scripts/Pooling/ObjectPool.cs"
gh-start="21" gh-end="34"
Poolable
The Poolable
class is an abstract class representing a poolable object. Each Poolable
object knows which ObjectPool
it belongs to, and it can be initialized, activated, and returned to the pool. When a Poolable
object is initialized, it's enqueued into its pool and deactivated. The Poolable
object must define two methods:
An
Activate
method, which activates the game object and sets its position and rotation.A
Return
method, which deactivates the game object and enqueues it in the queue of itsObjectPool
.
Github Code Section
gh-repo="BIRD-COMMAND/danger-space-public"
gh-branch="feature/portfolio-polish"
gh-file="Assets/Scripts/Pooling/Poolable.cs"
gh-start="12" gh-end="30"
In Conclusion
The object pooling system in Danger Space contributes to the game's performance by efficiently managing the lifecycle of frequently used game objects. While the individual performance benefits may seem small, they accumulate over time, and ultimately lead to a smoother and more consistent gameplay experience.
To illustrate this, I've included a comparison of the game's performance with (darker) and without (lighter) object pooling. The data was gathered using Unity's built-in profiler, and the resulting graphs were overlaid for easier comparison.
As you can see from the graph, implementing object pooling in Danger Space has led to a noticeable increase in the average frames per second (FPS). More importantly, the graph shows a significant reduction in FPS volatility, indicating that object pooling has made the game's performance more consistent and predictable.
The object pooling system is a clear example of how seemingly minor performance optimizations can have a significant impact on overall performance. By minimizing the overhead of continuous object instantiation and destruction, object pooling helps ensure that players of Danger Space enjoy a more seamless and performant experience, contributing to the overall quality and success of the game.