Optimization is needed for every game in order to make the game run well. Optimization is one of those endless voids where time goes to die. You can spend an infinite amount of time optimizing a game and it’s a good idea to not optimize early.
Early optimization wastes time and by the time that the game is finished the parts of the game that were optimized early may not be the actual bottleneck or may not even be in the game at that point. It’s important to make each part of the game “Good Enough” for now and then improve it later. This is true for everything in the game.
Perfect is the Enemy of Good.
Voltaire
It takes discipline to stick to this idea, though. As an artist, a programmer, as a craftsman, you want what you are working on to be the best that it can be. If you are an artist that could mean adding in all the detail that you appreciate. As a programmer that can mean making the code clean, as readable as possible for the next programmer that needs to understand your code, which is usually future you, or it can mean making the code run as fast as possible. It’s easy to get in a trap where you spend way more time optimizing something than is needed.
Is anyone going to notice the small detail that you put into that art? Is anyone going to notice that a non-critical part of the game takes 0.5ms to run rather than 1.5ms?
The answer is complicated because one of the most rewarding things about being a craftsman is having someone else notice the details, the passion, or the time and effort that you put into something. Since you want that positive feedback as a craftsman, it’s tempting to spend way too much time on something to make it the best that it can be.
Game Thread & Tick
One place where optimization was needed was the game thread, which is basically CPU usage. Honestly a surprise since we are using Unreal Engine 5 with real time global illumination via Lumen. Our initial assumption was that Lumen was going to cause the most issues for framerate because the Steam Hardware survey told us that the most common GPU that people had at the time was an NVIDIA GTX 1060 (or equivalent). And, also important, EPIC was targeting 30fps at Ultra Quality for Lumen on current generation console hardware, which is roughly 3x the performance of an NVIDIA GTX 1060 (as a side note: it looks like EPIC is going to do better than 30fps at Ultra on the current generation consoles, but it remains to be seen how complex developers make their scenes in real games).
In the previous Mortal Rite play test and demo builds, we had really bad CPU performance. Mortal Rite version 0.4.12 was the first version that we took the time to optimize CPU performance and at that point it was mainly cleaning up when actors in the game ticked. This was done in the middle of one of the play tests, which wasn’t great timing on our part since there were probably a lot of people that tried the play test and stopped playing due to poor framerates.
For those are are not aware: Each “thing” in the game ticks. A tick in a game is a frame. So, if something is ticking it is doing something each frame on the CPU. To optimize for 0.4.12, our tick count was optimized from over 400 to around 100. The count isn’t really important because you can have one really complex thing ticking and that can cause a performance issue by itself. But this illustrates a problem that in my estimation is an issue for anyone using Unreal Engine: Everything ticks by default and most things have multiple places where tick needs to be turned off (such as components and meshes). It feels like everything is working against you when it comes to tick because everything wants to “auto activate”, which starts tick (in some cases) even when you have set that “thing” to not tick.
Significance Manager
One way that we are dealing with game thread optimization is to have characters (for now) have the concept of Significance: How significant the character is for each frame.
We use a Significance Manager that has every character register with it and then on tick the Significance Manager determines how significant that character is.
We use the significance value to control how fast that character updates. No one will notice that an Enemy way off in the distance is being animated slower than one up close (especially if that someone is playing the game and not studying the pixels of that enemy in the distance), but having that Enemy in the distance update slower saves CPU time.
We haven’t dialed in the values for each character yet, but based on our early testing we’ve saved around 6ms (Stock AMD 5950x CPU) on the game thread by having characters tick less via significance and by cleaning up ticks again. This value of 6ms will vary from system to system (probably more than 6ms on our min spec machine! Will test that in the future) and will also depend on how many characters/enemies are close to your camera as the player, but this is huge.