Unity VR Optimization : Object Pooling

One problem we can run into when developing our games can occur when we Instantiate and Destroy hundreds of Objects in a short period of time. A good example of this would be having a gun that generates hundreds of bullets when we shoot a gun. The bullets would be Instantiated upon pulling the trigger and destroyed after X amount of time. 

So why is this such a big problem?

Seeing the Problem

In short, Instantiating and destroying Objects over and over again can be extremely taxing on the CPU. 

To demonstrate this, let’s look at this example scene I’ve set up. 

 

 

The Scene has a machine gun that is firing bullets at a rapid rate. Like we mentioned before, this involves Instantiating a bunch of Bullet objects and then Destroying them after X amount of time. Individually, these calls for memory allocation/freeing aren’t too taxing, but the summation of them eventually leads to FPS death.

 

This script has a few things going, but let’s focus on some primary functions. OnAwake will just grab and set the Rigidbody. The Start function will call ApplyVelocity(), which will in turn apply some velocity to our Rigidbody. Finally in Update, it will check if TimeUntilDestruction has reached zero and if it has, it will destroy the game object.

We’ll revisit this script later when we cover more about Object Pooling, but for now, we’ve covered what we need to know if we’re just Instantiating Bullets from our Spawner.

Speaking of which, let’s take a peek at the BulletSpawner for this example.

Spawning bullets is also very straight forward. Ignoring the items involved with Object Pooling, let’s start by going over the Start function. In Start, we use InvokeRepeating to call the InstantiateBullets function every 0.05 seconds. InstantiateBullets will determine if we’re using Object Pooling, and if not, it will then Instantiate a Bullet with the same position and rotation as itself. 

If we’re only Instantiating Bullets without any Object Pooling, that’s all we really need to know for this script. It seems nice and clean if we could ignore all the rest of that code, but there is a problem.

By going to Window -> Analysis -> Profiler, we can press play and see that we’re allocating a ton of memory constantly due to all these calls for Instantiating new game objects.

During a massive spike, we can see that the garbage collector is accounting for 26% of the CPU usage! The garbage collector is responsible for freeing up any memory that is no longer being allocated for use. In this example, it would be us freeing up memory after we Destroy any Bullet object.

So what’s the solution?

Object Pooling

 

The idea behind Object Pooling is simple. Instead of creating and destroying objects constantly, we instead create Objects as needed (or up front) and put them in a pool (array, list, ect.). When we need an Object, we request one from the pool, use it for X amount of time and return it when it’s no longer needed. It will then stay stored in the pool until it’s needed again.

This may cost a little more in memory since we’re storing a pool of Objects, but it greatly reduces the cost on the CPU since it no longer needs to allocate and deallocate memory hundreds of times.

Unity 2021 provides it’s own implementations of Object Pools with a wide variety to pick from depending on what scenario we’re using them for. For this tutorial, we’ll just do a brief introduction into using a generic Object Pool.

Constructing a Object Pool

In order to use Unity’s Object Pool class, we need to construct it from the template provided. The key functions needed at a minimum are a Create Function, Action on Get function and an Action on Release. 

Zooming in, we can see at the top that we initialize our _bulletPool variable by calling the constructor for a new ObjectPool<Bullet>. We then assign three different functions that it requires. 

The first is a function that will Instantiate a new Bullet anytime the Object Pool needs one. If we were to call the Get function for this Object Pool and no Object was found, the Object Pool would then use this function to add one to the Pool.

The second function is a Action On Get, which passes in a Bullet object that we can then manipulate for whatever we need. In this example, will set the Bullet pulled from the pool to the position and rotation of the Spawn Point, it will call the Bullet’s SetPool function (which I’ll explain in a moment) and finally set the Bullet’s Active state to true.

Using SetPool for our Bullet that we’ve grabbed from the ObjectPool will allow us to be able to return the Bullet to the Object Pool instead of destroying it. We can see in the DestroyBullet function that it will check if we’ve set the Bullet’s pool and if we have, we just release it.

The third function we set is for when we need to Release the object back into the pool. We do this by having a function that takes a Bullet object and setting its Active state to false.

With those three functions set, we have what we need for a basic Object Pool to work. 

Seeing it in Action

Now all we need to do is check the box that asks if we’re using Object Pooling in our Spawners and let’s see what we got!

Opening up the Profiler again (Window -> Analysis -> Profiler) and pressing play, let’s see if there are any improvements.

The initial frames will have a few memory allocation calls due to us filling up the pool, but once it reaches capacity, we’re no longer initializing memory nor are we using massive garbage collection calls!

The Object Pool is working! We can further see the Object Pool in action by looking at the editor, which I’ve set up to display the pools both Active and Inactive objects.

As we can see, the Object Pool reaches up to about 100 and then begins to cycle through the Bullets as the de-spawn and are again retrieved from the pool.

Conclusion

Object Pooling is a great tool to use whenever we encounter a scenario where we need to create and destroy a ton of objects repeatedly. We’ll save a ton of time and CPU resources just by storing our objects for later use with only a small increases to our use in total overall memory being used.

This one took way longer than expected… so I really hope you found it useful!