Many novice indie developers are too late to think about code optimization. It is given at the mercy of engines or frameworks or is considered as a “complex” technique, inaccessible to their understanding. However, there are optimization methods that can be implemented in a simpler way, allowing the code to work more efficiently and on more systems. Let's first consider the very basics of code optimization.
Optimization for players and their own mental health
Quite often, indie developers mimic the optimization methods of large companies. This is not always bad, but the desire to optimize the game after passing the point of no return is a good way to keep yourself crazy. Smart tactics for tracking optimization performance will be segmentation of the target audience and study the characteristics of its machines. Benchmarking games with regard to computers and consoles of potential players will help to maintain a balance between optimization and your own mental health.
Code Optimization Basics
In fact, there are a fairly small number of optimizations that can almost always be used to increase the speed of the game. Most of them are not tied to a specific platform (some engines and frameworks take them into account), so below I will show examples in pseudocode so that you know where to start.
Minimize the impact of off-screen objects
Often the engines are engaged in this, and sometimes even the GPUs themselves. Minimizing the amount of computation for off-screen objects is extremely important. In our own architecture, it is better to divide objects into two “layers” - the first will be a graphical representation of the object, the second - data and functions (for example, its location). When an object is off-screen, we no longer need to spend resources on rendering it and just keep track of it. Tracking with variables such as position and state greatly reduces resource requirements.
')
In games with a large number of objects or objects with large amounts of data, it may be useful to take another step and create separate update procedures. One procedure will perform the update when the object is on the screen, the other when it is outside. By adjusting this separation, we can save the system from having to perform a variety of animations, algorithms, and other updates that are optional when the object is hidden.
Here is an example of the pseudo-code of an object class using flags and location constraints:
Object NPC { Int locationX, locationY;
Although this example is very simplified, it allows us to interrogate objects and determine their visibility before drawing, so you can perform a simplified function instead of making a full draw call. To separate functions that are not graphical calls, it may be necessary to create an additional buffer — for example, a function that includes everything that a player can see soon, and not just what he can see at the current moment.
Frame update independence
In engines and frameworks there are usually objects updated in each frame or “cycle” (tick). This is very processor-intensive, and in order to reduce the load, we should, if possible, get rid of the update in each frame.
The first thing to separate is the rendering function. Such calls are usually very active use of resources, so the integration of the call, telling us whether the player’s visual properties have changed, greatly reduces the amount of rendering.
You can take one more step and use a temporary screen for our objects. By drawing objects directly into a temporary container, we can guarantee that they will only be drawn when necessary.
Similar to the optimization mentioned above, a simple poll is used in the initial iteration of our code:
Object NPC { boolean hasChanged;
Now, in each frame, instead of performing a multitude of functions, we first see that this is necessary. Although this implementation is also very simple, it can significantly improve the efficiency of the game, especially when it comes to static objects and slowly updated objects like HUD.
In your game, you can go even further and break the flag into several smaller components to segment the functionality. For example, you can add separate flags to modify data and graphical changes.
Direct calculations and search for values
This optimization has been applied since the very first days of the gaming industry. Choosing a trade-off between calculations and the search for values can significantly reduce processing time. In the history of gaming, a well-known example of such optimization is storing the values of trigonometric functions in tables, because in most cases it is more efficient to store a large table and retrieve data from it rather than perform calculations on the fly, which increases the load on the processor.
Today, we rarely have to make a choice between storing results and executing an algorithm. However, there are still situations in which such a choice can reduce the amount of resources used, which allows adding new features to the game without overloading the system.
Implementation of this optimization can be started by determining the often performed calculations in the game or parts of the calculations: the more calculations, the better. Running the repeating parts of the algorithm once and storing their values can often save a significant amount of computational resources. Even highlighting these parts into separate game loops helps optimize performance.
For example, in many top-down shooters there are often large groups of enemies performing the same actions. If there are 20 enemies in the game, each of which moves in an arc, then instead of calculating each movement separately, it will be more efficient to save the results of the algorithm. Because of this, they can be changed based on the initial position of the enemy.
To understand whether this method will be useful in your game, try using benchmarks to compare the difference in resources used in calculations and data storage.
CPU idle time usage
This is more relevant to the use of inactive resources, but with proper implementation for objects and algorithms, you can position tasks in such a way as to increase code efficiency.
To start applying downtime sensitivity in your own software, you first need to highlight those in-game tasks that are not time-critical and can be calculated before they are needed. First of all, it is worth looking for a code with similar functionality in what relates to the atmosphere of the game. Weather systems that do not interact with geography, background visual effects, and background sound can usually be referred to as idle computing.
In addition to atmospheric calculations, the area of computation during idle times includes compulsory calculations. It is possible to make more efficient the calculations of artificial intelligence occurring independently of the player (because they either do not take into account the player or do not interact with the player yet), as well as calculated movements, such as script events.
Creating a system that uses idle mode does not just provide increased efficiency — it can be used to scale visual quality. For example, on a weak machine, the player can only have access to the basic (“vanilla”) gameplay. However, if the system detects frames in which almost no calculations are performed, then we can use them to add particles, graphic events and other atmospheric strokes, which give the game more pathos.
To implement this possibility, you need to use the functionality available in the selected engine, framework or language, allowing you to determine how much the processor is used. Set flags in your code that allow you to easily determine the amount of "extra" computing resources and configure the subsystems so that they check these flags and behave accordingly.
Combination of optimizations
By combining these methods, you can make the code much more efficient. Thanks to efficiency, it is possible to add new functions, compatibility with a large number of systems and ensure high-quality gameplay.