Instantiate and Destroy. Two incredibly useful functions that can wreak havoc on performance when improperly used together. Let’s look at how I devastated the performance of “Tactical Twitch”, and what I’m doing to optimize it.
WARNING: You might learn something… Maybe.
A Little Background
One of my goals for “Tactical Twitch” is to provide an old school, NES era vibe in the game mechanics. To assist in this goal, I want slow bullets. Remember those? The sort of bullets one can see, and jump to dodge if they’re quick enough. To make this work, my bullets are GameObjects with their own art work and collision boxes. This allows the player to move around and avoid incoming fire; as appose to a Raycasting solution that would calculate whether or not a bullet hit based on line of site.
Everything from bullets to grenades, rockets, and enemies are their own game objects. These objects are used frequently. In a heated moment there could be a multitude of ammunition in use, and enemies that die and spawn. I wrote the code for this a while ago. When I was last working on this project, I was using a high end desktop. As we approach Summer I’ve been trying to knock out smaller “to-do” tasks in preparation of resuming development. I’m currently working on my laptop which has a mid range i5, and Intel Graphics. My frame-rate is in the upper 20s. It’s playable, but not really enjoyable. I’ve started to fix this, but first I had to identify the performance issues. Spoiler alert, it’s largely my abuse of Instantiate and Destroy.
Sources That Helped Identify Issues(s)
I’ll quickly walk through the posts that helped me realize my problem.
- Reddit : “What Can I do to Make My Game Slow and Inefficient?” – This archived thread is full of insight. I highly recommend reading the whole thing, and reflecting on one’s own code and art practices. To be honest, I already knew Instantiate and Destroy are costly. I just didn’t realize how costly they were, or how bad it was hurting performance until I began working on a mid-range laptop. This thread has solid insight, and pointed me in the right direction.
- Unity 3D Forum : “Is Instantiate bad for performance?” – This post reaffirmed my understanding that Instantiate is an integral part of making a game; however, it clarified the fact that there are good practices one should observe when using it.
These two threads helped me identify the logic I should have been using when working with these functions. It’s actually a pretty simple idea. Reuse what already exists, instead of instantiating and destroying on the fly. I think it’s easy enough to say and understand, but what does it look like in practice?
Solution – Instantiate, Pool, Reuse
In my game, certain scenarios can lead to the simultaneous instantiating and destruction of a multitude of objects. This isn’t a rare occurrence, these specific situations happen often. What I ended up doing to improve this is the following:
- I have an empty game object in my scene that I call “Game Master”. This object contains my script that manages the level environment. In this script I declare built-in arrays for bullets, rockets, grenades, and enemies. All of which are of the GameObject type.
- Whenever one of these items is to be used, the first thing I do is check the length of the appropriate array. For instance, if I fire a rocket I check the length of my rockets array. If the length of the rockets array is greater than zero, that means we have at least one on tap. The rocket is pulled from the array, it’s new transform position is set, and it’s values are initialized. Rocket away!
- When the rocket hits it deals damage. Then, instead of destroying itself, it resets all of its values, places its transform position outside of the playable area of the level, and reinserts itself in the rockets array.
- In the event that the array length is zero, that means that either none are ready or none exist. In either case, we need to make one. This is now the only time Instantiate is used during game-play.
To take this further, I pre-populate my arrays with about 8 items each. All of these instances are created while the level is loading, and not during play. Now, Instantiate only executes if all 8 of any given type are currently in use. Then the newly instantiated rocket adds itself to the array, increasing the pool from 8 to 9. I’ve gone from a ridiculous amount of Instantiate and Destroy calls to a rare Instantiate and no Destroy calls.
My ammunition, my enemies and items all get recycled into their appropriate arrays that serve as an availability pool. Doing this has greatly increased my performance on lower end devices. While the slow down wasn’t noticeable on my desktop, frame rates dropped dramatically with heavy action on my laptop. That is no longer the case. I still have a few other areas I need to optimize, but transitioning my code and logical thinking to this idea has already shown its benefit. As I clean up and optimize other areas, I’ll be sure to write up more posts.
Additional Thoughts & Conclusion
It seems odd to me, that adding/removing from an array is better for performance than rapidly instantiating and destroying game objects. When I pull something from the array, I’m copying it to a variable. Isn’t that an instance? Likewise, when I remove the copy from the array, isn’t that destroying? I’m not directly using the functions, but I would think it’s the same concept.
To better answer these questions, I may try a solution that does not require copying game objects to variables and changing the array size. As an example, if I were to loop through the array, and simply use the first game object that isn’t already in use, I could probably achieve the same effect. The the question then becomes “is looping through an array of objects every time an item is used more costly than my current solution?” Only one way to find out. I’ll update this thread if I’m able to get better results on this issue.
If you’re reading this and thinking of better solutions – I’d love to hear about them. I encourage you to leave your thoughts in the comments. I love constructive input that serves to make better games.
I hope this helps some of us, and thanks for reading!
***UPDATE 2/25/2016 ***
Quick Update on one of my last thoughts in this article:
In this post I pondered why my solution worked better than Instantiate and Destroy. I was thinking that the object is instantiated and destroyed internally, in order to pop it in and out of the array.
I was talking with a friend about this, and he reminded me of an important point. The variable and the array index are not actually holding the object. The object exists in memory. The variable and/or array index are simply pointing to that same object; so, the object isn’t instanced or destroyed. I’m simply changing the way I point to the object.
Now the picture is complete.