I’ve been postponing this for a long time, but some time near the end of the past year i realized another overhaul is due, this time for optimization purposes. It’s a long and tedious process and can get you into a rabbit hole that can be hard to get out of. My recommendation is not to overoptimize, but do take care of bottlenecks as soon as they appear since they can get you back to square one too late in the development to fix in a reasonable amount of time. Plan your design accordingly and optimize as soon as you finish an enemy design or feature, that way you lay a solid foundation for future content creation that will utilize the same, optimized framework so you don’t have to worry too much later on.
Avoiding Instantiate and Destroy by using Pooling
Pooling is a great tool to avoid those dreaded instantiate/destroy calls. You’d think that computers in 2020. can chew up pretty much anything you throw at them, but it’s not that simple. Whenever you create a new object (in our case, a prefab instance) it is stored into memory (with resources taken to do the operation itself and memory taken to actually store it) along with the pointers that, well, point to that part of the memory so that it can be found and used whenever needed. Of course, memory manager needs to find the unused part of memory to use and clear it up before use. That process is called Garbage Collection and it’s the usual suspect in most of the performance problems. It all happens quite fast, literally every frame, so if you ever used profiler in Unity you will see a GC Alloc column which tells us how much garbage is generated every frame. You might see a value of 2-3 kilobytes and think “oh, that’s not much”, but bear in mind that if you have 60 frames per second that’s 120-180 kilobytes of garbage per second. Per minute, that’s 7-10 megabytes, so without collecting the garbage your game would probably slow downs and crash quite soon.
Luckily for us, Garbage Collection in Unity is automated so you won’t have memory leaks which will lead to crashing. Automated means you don’t need to clear stuff out of the memory manually, but, unuckily, it uses garbage collector that stops the execution of the game code (though since Unity 2019 you can use the Incremental Garbage Collection which can alleviate these issues if your garbage footprint is small enough). You will see those exact moments in your profiler as a spike and your game will stutter, which is annoying for the player.
So, only reasonable solution to this problem is to avoid creating garbage. But how can you do it if you are creating new bullets, enemies, asteroids and pickups all the time? By reusing them, of course! It is called Pooling (since you use a “pool” of objects) and it is a old but gold method for boosting performance in video games.
It’s not too hard to grasp, you simply instantiate all the objects you will be using on the start of your game (a bit simplified, it takes some time to load everything so you should do it in segments in appropriate time, but more on that later) and then you simply activate them and deactivate them (colloquially called spawning and despawning). Such a simple solution, but with an expected caveat. When you deactivate an object, it keeps all its properties from the last frame of its active state. So, the despawned enemy will keep its position, rotation, hit points, animator states, you name it, and next time you activate an object it will be in the same state as when deactivated, rotated in unusal direction, with 0 HP and animator stuck on same frame in the middle of an animation. So what we need to do is reset everything to its original state before deactivating the object. Seems simple enough, though it can get complicated if you have object that have children and they have their own children and so on, which is usually the case.
In this simple diagram above we can see an enemy ship with two children – Jet and Gunpoint. They are all spawned and need to be despawned. But what is important here is the order of operation. When we first spawned them, we spawned the ship first, then the Jet and Gunpoint as children. So we need to do that in reverse order because if we deactivate the ship first, children will be deactivated too before they get the chance to reset their variables.
When the ship takes enough damage to be destroyed, it sends the event (don’t worry, sending events is quite cheap in Unity, hoorah) to both Jet and Gunpoint to reset their variables and despawn themselves. They can either send the event that they finished their reset process back to the parent (ship), or the ship can wait a few frames for them to finish and then reset its own variables and despawn. That way, we have a nice, clean object ready to be reused for the next ship that will be spawned.
Don’t think that pooling alone will help your game magically run at 300 FPS since it’s not the only part of the equation of successful performance management but it does eliminate the worst offenders – instantiate and destroy calls.
If you’d like to read more on the subject, i recommend a great article by Mark Placzek that i often like to read again. It also has some good technical tips on creating your own pool, but if you’re a one man army like me i really recommend using a plugin for pooling from the asset store, they work great out of the box, they are performant and they are easy to use. The one i use is Pool Boss but i can also recommend Pool Kit if you have 40+ cookies to spare.
For more stuff on memory management and garbage collection in Unity i recommend their own manual which you can find here, it’s a cool read to get into the innards of how things work under the hood.