A very important aspect of software development, whether in games or applications, is the process of optimization. I'll discuss this briefly, but don't expect a lengthy tutorial (I am sure there are far better specialized tutorials out there). However, I'll perhaps provide some guidance about approaching it.
Additionally, I'll go over some new changes and updates in the Forest area for VoidGate, along with a few other small, yet significant, changes.
If one were to simplify the process of programming it could be put into 3 steps, it would probably look like this:
- Get it working right
- Clean it up
- Make it efficient
While it is good practice to keep in mind the cost of using certain algorithms, especially in terms of the Big O, it is far more important to simply get your current problem solved and working. Thinking about optimization while struggling to get a particular feature or function working could very well be a waste of time.
In the case of Big O, it's best to consider how to approach the problem before implementation. Other than that, it is very difficult to determine performance impact of any function/features before its implementation.
Trying to optimize a particular part of your program before knowing if it actually causes a bottleneck is like looking for a needle in ten stacks of hay, and not knowing which haystack contains it.
This is where the powers of profiling come into play. Profiling should be a tool every developer becomes familiar with; it removes the guesswork and gives precise details on how to optimize your program.
Since VoidGate is being developed in the Unreal Engine, we use Unreal's profiler. All profilers essentially work the same.
So let's take a look at a screenshot of profiling pre-optimizaiton:
As we can see here, the major bottleneck in the game at this time is on the primary GameThread, particularly AI (monster/enemy) movement and animation.
At of the time of this writing, the randomized Forest Area in VoidGate spawns approximately 200 enemies on the map. So this bottleneck is understandable, though obviously not ideal. So optimization comes down to reducing the performance time.
There are a few options to consider. First, rather than spawning all enemies at the beginning, we can spawn them as they are needed. For example, we can check when a player nears an area where a particular set of enemies reside, then spawn them as the player reaches it.
However, this option has flaws for a number of reasons. First spawning actors (instantiation of objects) is an expensive process. This is because memory allocation for the object is slow. Even with Unreal's specialized pooling it is a slow process. Now, if we could specifically choose when to spawn an object in the game (such as during a loading screen, cinematic, etc) it would be a viable option. However, during gameplay this is impractical. If we spawn the enemy while the player is in the middle of combat (or even outside) there will be a very noticeable frame rate dip. If we spawn the enemy when the player has to fight it, we'll get a frame rate dip and an enemy potentially popping out of nowhere.
I spent a brief amount of time trying to find how others might approach this issue and couldn't find anything. It doesn't mean no one has addressed it and has tackled the problem, I just couldn't find it.
So I came up with my own approach (even though it's highly probable I'm just reinventing the wheel here). Memory is cheap and not really an issue right now in the game, so it is safe for us to spawn all of the enemies during the map's load time.
After this, we simply have each enemy do a check regarding their distance to the player. It's important to note this check is not done every frame or tick; checking distance can be an expensive operation, especially if Unreal uses the accurate square root method (which I assume it does; Manhattan distance is cheaper, but also less accurate).
First we'll go over the result of the check, then we'll go over the timing of the check.
So, once we have our result of the distance between the enemy and the player we have a few different options. I base this on three tiers of distance: short, medium, and far. I use these rather than actual values because it will depend on the game, and the medium distance may be unnecessary in other cases.
VoidGate has stealth mechanics, though not all of it is fully implemented yet, and AI is perception based (hearing and sight) and uses alert levels of awareness.
This means short distance needs to be far enough for the player to be aware of an enemy and either engage or avoid. Since the game depends heavily on randomization, medium distance is far away enough the enemy doesn't need to be visible, have active animations, or have active perceptions, but close enough to still move around. And far distance means the enemy/AI is completely inactive.
Now, medium distance could probably use more optimization however, for the sake of simplicity, we won't go into that.
- Short - Enemy visible and fully active, perceptions are active (fully active).
- Medium - Enemy not visible, animations not active, perceptions inactive, brain still active.
- Far - Fully inactive.
So, how do we do the timing to check? In my case, I use Unreal's event timers. The frequency is determined by the distance. The minimal amount of time is half a second, while the maximum amount of time is thirty seconds. These bounds are set by simple clamping.
Then I use a formula to determine the amount of time: Time = (Distance - ShortDistance) / TimeDistanceRatio.
There is no magic to this formula, and it certainly can be changed. I only picked it by asking: At what distance do I want to make a check every second? In Unreal terms, each Unit (1.0) is approx. 1 cm in distance. Let's say my short distance is approximately 5000 units, this means as long as the enemy is within this distance we want to do a check at our minimal amount, up to that amount.
For example, let's set TimeDistanceRatio to 2000. This means for every 1000 units past the short distance, half a second will be added to the amount of time.
The rationale behind this is simple: it is improbable that a player reach a certain distance to far enemies in a certain amount of time, so the farther they are the less frequent we need to do the check. But as the player gets closer to the enemy, we need to make the check more frequently.
So, what is the result of doing this method? Obviously the results will vary according to the formula, variables, and distance among other things. I could certainly play around with the formula and values for different result, however I was happy with the result I got on this basis.
As you can see, the amount of time spent in the GameThread was cut in half, from 10.542 ms to 5.275 ms. The majority of enemies now have their performance time cut in half, even when "moving."
Of course, this doesn't mean the job is done. The instructions for game optimization can be found on bottles of shampoo: Lather, rinse, repeat. It's not something that needs to be done after every step, but often enough. Too much and progress will be slow, too little and progress won't matter much. The when fits inside a vague Goldilocks zone.
There could very well be, and probably are, better ways to do this. Please leave your solutions or suggestions in the comments.
Now we can move onto some recent changes.
First, we have implemented a feature to improve the camera view. Since our game depends on randomized level generation, we can't design our levels to avoid obstructing the camera. There are a number of other ways to deal with this, however, we'll stick to our own solution.
Whenever an object obstructs the camera it becomes translucent, so the player won't lose visibility. Special thanks to this YouTuber for the method and base setup on doing this:
I recommend if you use this for your own project that you modify it to suit your particular game, as I have. The code is built for general use, so taking some time to fit it around your own project will increase its efficiency. As an example, the code has you select a particular Actor class to filter through. In our case, we don't even care about the class. Rather than have Unreal fetch any actors matching the class and iterating through an array, we only need one actor. So we grab it directly and remove the array iteration. While the performance difference may be negligible in this case, it is good practice.
Additionally, there is a flaw in the basic setup that ALL static objects will be susceptible to this (allowing the camera to break through the ground to look up at the player, though this might be what you want). And it requires the spring arm on your camera to ignore any collision. This can cause problems. For example, in our dungeon level we don't want the player to be able to swing the camera outside of dungeon walls and examine the "outside" of it, allowing them to view dungeon layout among other things.
So we managed this by setting up a new object type within Unreal's custom channels called Fade, referring to the code's fade ability. Any object we want to use this effect, we set its object type to Fade. There will be a few other tweaks you might require, such as properly setting collision on your pawn/character.
Other than that, the code is well-designed. It uses Unreal's tracing system to search for objects, which Epic has demonstrated has little impact on performance.
And finally, we would like to mention a small update to the Forest Area and other similar maps. These maps generate a pathway to connect important parts of the map; offering guidance to the player. Enjoy some screenshots.