Posted October 05, 2024 by Board Gamers
#Object Pooling #Optimization #AI #C++
Considering the nature of our project and specifically my contributions being our enemy AI, I decided to spend the first week of this month refactoring and thinking about optimization. What brought me to this decision was first of all realizing that my AI are going to be the most performance heavy area of our game since there will be more than 100 of them on screen at any given time. I also was noticing that as we began adding more complexity in logic and having effects both visual and auditory, our framerate was starting to tank and I knew a large part of that was on me.
My first idea for optimization was to implement the object pooling design pattern. In summary this means we will no longer be constantly spawning and destroying enemies. Instead we spawn the maximum number of enemies we would ever want in the very beginning of the game, and recycle them rather than destroying them.
To achieve this we set our characters to now derive from a "PooledCharacter", which would toggle whether characters should be "InUse" or not based off of conditions in the game. Then we made a new actor component that would house our pool. In this component we assign which Actor class we want to spawn and how large the pool is. We then spawn that amount of whichever class was passed in and set them all to be not in use. When these actors are not in use we hide them in game, disable their tick and their collision, unpossess them from their controller, and disable their navigation and animation. Then we simply do the opposite when they are active.
In this image you can see that we have a small pool of enemies spawned here in the scene yet they are not rendered or running any logic.
Our spawn manager begins to activate them over time, which triggers our pooling logic for our enemies, rendering them, turning on their tick and collision, and assigning them a controller, rather than spawning them.
When we kill the enemies they unpossess their controller and collision is disabled.
After a set amount of time the enemies are set to be inactive and they are sent back to the pool, which completes the loop and this process starts over when we need new enemies to spawn. One consideration this leads to is how many enemies should we put in our pools? Since we are constantly spawning different enemies based on probability I decided that for our most frequent enemy their numbers should be close to whatever the spawn limit is and for our more bulky enemy that spawns at a tenth of the rate, I decided about a fourth of the spawn limit is safe. These numbers will have to be tested to make sure we are being as efficient as possible.
This definitely helped our performance, but I was still having issues when we started pooling around 80 - 90 enemies. I had my suspicions about what areas of my current work could be problematic, but to be sure I utilized the Unreal Insights tool to profile our performance and see exactly what the majority of our resources were being spent on.
The results of the profile both confirmed my suspicions and surprised me with problems I wasn't aware of. Unsurprisingly one of our most expensive areas was in the Tick function, but I also discovered that my animation instances and shockingly my character movement component were also taking up a significant amount of resources. I did some research and learned something that was as exciting as it was disheartening.
There were dozens of various posts talking about why using the Character class in general for large amounts of enemies is problematic, mostly due to the character movement component. There is a ton of functionality built into this class that for our purposes we just don't need such as support for flying and swimming. It also has a much more expensive tick function since it needs to accommodate for all of the extra unnecessary logic. This is usually okay when you have maybe up to a dozen enemies, but when you have hundreds it becomes very expensive.
I spoke with my team about my findings and came to a decision that would kill two birds with one stone, but would mean that I would have to postpone creating new features for the time being. I had been wanting to start converting my blueprints to C++ which by itself would help speed up our game. But I also took this as an opportunity to convert my enemies to the Pawn class, and give them a floating pawn movement component. This new component is much lighter than the character movement component and offers all of the same functionality that we currently needed for our logic. I am still in the process of conversion but should be done by the start of next week.