Game code optimization basics

Original author: Kyle Speaker
  • Transfer
image

Many beginner indie developers think too much about code optimization too late. It is 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 start by looking at 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 drive yourself crazy. A smart tactic for tracking optimization performance is to segment the target audience and study the characteristics of its machines. Benchmarking games based on computers and consoles of potential players will help 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 on pseudo-code so that you know where to start.

Minimizing the impact of objects off the screen


Often the engines do this, and sometimes even the GPUs themselves. Minimizing the amount of computation for objects outside the screen is extremely important. In your own architecture, it is better to separate objects into two “layers” - the first will be a graphical representation of the object, the second will be data and functions (for example, its location). When an object is off the screen, we no longer need to spend resources on its rendering and just track it. Tracking variables such as position and status significantly 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 setting up such a separation, we can save the system from the need to perform many animations, algorithms, and other updates that are optional when the object is hidden.

Here is an example of an object class pseudocode using flags and location restrictions:

Object NPC {
    Int locationX, locationY; //текущая позиция объекта на 2d-плоскости
	Function drawObject() {
		//функция отрисовки объекта, вызываемая в цикле обновления экрана
	}
	//функция, проверяющая, находится ли объект в текущем вьюпорте
	Function pollObjectDraw(
        array currentViewport[minX,minY,maxX,maxY]
        ) {
	//если он находится внутри вьюпорта, сообщаем, что его нужно отрисовывать
		If (this.within(currentViewport)) {
			Return true;
		}
		Else {
			Return false;
		}
	}
}

Although this example is very simplified, it allows us to query objects and determine their visibility before rendering, so that we can perform a simplified function instead of making a full draw call. To separate functions that are not graphic calls, you may need 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 moment.

Frame update independence


Engines and frameworks usually have objects that are updated in every frame or “cycle” (tick). This heavily loads the processor, and to reduce the load, we should get rid of the update in every frame whenever possible.

The first thing to separate is the rendering functions. Such calls usually use resources very actively, so the integration of a call telling us if the player’s visual properties have changed has greatly reduced the amount of rendering.

You can take another step and use a temporary screen for our objects. By rendering objects directly to a temporary container, we can guarantee that they will be drawn only when necessary.

Similar to the optimization mentioned above, a simple poll is used in the initial iteration of our code:

Object NPC {
    boolean hasChanged; //флаг имеет значение true, когда в объект внесены изменения
	//функция, возвращающая флаг
	Function pollObjectChanged(
		return hasChanged();
	}
}

Now, in each frame, instead of performing many functions, we first make sure that it is necessary. Although this implementation is also very simple, it can significantly increase the effectiveness 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 for segmenting the functionality. For example, you can add separate flags to modify data and graphic changes.

Direct calculations and search of values


This optimization has been applied from the very first days of the gaming industry. By choosing a trade-off between computing and finding values, you 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 get 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 you to add new features to the game without overloading the system.

The implementation of such an optimization can be started by identifying frequently performed calculations in the game or parts of the calculations: the larger the calculation, the better. Running the repeating parts of the algorithm once and storing their values ​​can often save a significant share of computing resources. Even highlighting these parts in separate game cycles helps optimize performance.

For example, in many top-down shooters, there are often large groups of enemies performing the same actions. If the game has 20 enemies, each of which moves in an arc, then instead of calculating each movement separately, it will more efficiently save the results of the algorithm. Due to this, they can be changed based on the initial position of the enemy.

To understand if this method will be useful in your game, try using benchmarks to compare the difference in resources used in computing and storing data.

CPU Downtime


This applies more to the use of inactive resources, but if implemented correctly for objects and algorithms, you can arrange tasks in such a way as to increase the efficiency of the code.

To start applying sensitivity to downtime in your own software, you first need to highlight those in-game tasks that are not time critical and can be calculated before they become needed. First of all, you should look 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 downtime calculations.

In addition to atmospheric computing, the field of computing during downtime includes mandatory computing. It is possible to make the computations of artificial intelligence, independent of the player, more efficient (because they either do not take the player into account, or until they interact with the player), as well as calculated movements, for example, scripted events.

Creating a system using 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 access 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 touches that give the game more pathos.

To implement this feature, you need to use the functionality available in the selected engine, framework or language, which allows you to determine how much the processor is used. Set flags in your code that make it easy to 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 features, compatibility with a large number of systems and ensure high-quality gameplay.

Also popular now: