For many starting indie devs, code optimization becomes almost a second thought. It gets passed over in favor of engines or frameworks, or can be considered a more ‘advanced’ technique out of their reach. However, there are optimization methods that can utilized in more basic ways, allowing your code to operate more efficiently and on more systems. Let’s take a look at some basic code optimization to get you started.
Optimizing for Players and Sanity
It isn’t uncommon for indie developers to emulate the optimization methods of larger companies. It’s not necessarily a bad thing, but striving to optimize your game past the point of useful returns is a good way to drive yourself mad. A smart tactic to keep track of how effective your optimizations are is to segment out your target demographic and look at the types of specifications their machines have. Benchmarking your game against the computers and consoles that your potential players are using will help to keep a balance between optimization and sanity.
Basic Code Optimizations
All that aside, there are quite a few optimizations that can almost always be used to help your game perform better. Most of these are system agnostic (and some engines and frameworks already take them into account), so I’ll include some pseudocode examples to get you off on the right foot. Let’s take a look.
Minimizing Off-Screen Object Impact
Often handled by engines, and sometimes even GPUs themselves, minimizing the amount of processing power going into off-screen objects is extremely important. For your own builds, it’s a good idea to start separating your objects into two “layers” essentially—the first being its graphical representation, and the second being its data and functions (such as its location). When an object is off-screen, we no longer need to spend the resources rendering it, and instead we should opt for tracking. Tracking things, like location and state, with variables reduces the necessary resources to only a fraction of the initial cost.
For games with a large number of objects, or objects that are data-heavy, it may even be useful to take it a step further by creating separate update routines, setting one to update while the object is onscreen and the other for off-screen. Setting up a more advanced separation in this way can save the system from having to execute a number of animations, algorithms, and other updates that may be unnecessary when the object is hidden.
Here’s a pseudocode example of an object class using flags and location constraints:
Object NPC { Int locationX, locationY; //current location of the object on a 2d plane Function drawObject() { //a function to draw your object, to be called in your screen update loop } //function that checks if the object is within the current view port Function pollObjectDraw( array currentViewport[minX,minY,maxX,maxY] ) { //if it is within the viewport, return that it can be drawn If (this.within(currentViewport)) { Return true; } Else { Return false; } } }
Although this example is simplified, it allows us to poll whether the object will even show up before drawing it, letting us run a fairly simplified function instead of a full draw call. To separate functions besides graphical calls, it may be necessary to utilize an additional buffer—for example, a function that would include anything a player may be able to see shortly, rather than what they are just currently able to see.
Separating From Frame Updates
Depending on the engine or framework that you are using, you may commonly have objects updating on every frame, or “tick”. Doing so can tax a processor pretty quickly, so to ease that burden we’ll want to reduce calls on every frame whenever possible.
The first thing we’ll want to separate is function rendering. These calls are typically resource intensive, so integrating a call that can tell us when an object’s visual properties have changed can reduce rendering drastically.
To take it a step further, we can utilize a temporary screen for our objects. By having the objects directly draw to this temporary container, we can ensure that they are only being drawn when necessary.
Similar to the first optimization mentioned above, the starting iteration of our code introduces simple polling:
Object NPC { boolean hasChanged; //set this flag to true whenever a change is made to the object //function that returns whether Function pollObjectChanged( return hasChanged(); } }
During every frame now, rather than performing a number of functions, we can see whether it is even necessary. While this implementation is also simple, it can already start to show huge gains in your game’s efficiency, especially when it comes to static items and slow-updating objects like a HUD.
To take this further in your own game, breaking the flag down into multiple, smaller components may be useful for segmenting functionality. For example, you could have flags for a data change and a graphical change occur separately.
Live Calculations vs. Look Up Values
This is an optimization that’s been in use since the early days of gaming systems. Determining the trade-offs between live calculations and value lookups can help to reduce processing times drastically. A well-known use in the history of gaming is storing the values of trigonometry functions in tables since, in most cases, it was more efficient to store a large table and retrieve from it rather than doing the calculations on the fly and putting additional pressure on the CPU.
In modern computing, we rarely need to make the choice between storing results and running an algorithm. However, there are still situations when doing so can reduce the resources being used, allowing for the inclusion of other features without overloading a system.
An easy way to begin implementing this is to identify commonly occurring calculations, or pieces of calculations, within your game: the larger the calculation, the better. Performing recurring bits of algorithms a single time and storing it can often save sizable amounts of processing power. Even isolating these parts into specific game loops can help to optimize performance.
As an example, in many top-down shooters there are often large groups of enemies performing the same behaviors. If there are 20 enemies, each moving along an arc, rather than calculating each movement individually it’s more efficient to store the results of the algorithm instead. Doing so allows it to be modified based on each enemy's starting position.
To determine if this method is useful for your game, try using benchmarking to compare the difference in resources used between live calculation and data storing.
Utilization of CPU Idleness
While this plays more into the utilization of dormant resources, with careful thought for your objects and algorithms, we can stack tasks in a way that pushes the efficiency of our code.
To begin using sensitivity to idleness in your own software, first you’ll have to separate which tasks within your game are not time-critical or can be calculated before they are needed. The first area to look for code that falls into this category is functionality that is strictly related to the game’s atmosphere. Weather systems that don’t interact with geography, background visual effects, and background audio can all fit into idle computation easily.
Beyond items that are strictly atmospheric, guaranteed calculations are another type of computation that can be placed into idle spaces. Artificial intelligence calculations that are going to occur regardless of player interaction (whether because they don’t take the player into account, or they are unlikely to require player interaction as of yet) can be made more efficient, as can calculated movements, such as scripted events.
Creating a system that utilizes idleness does even more than allowing for higher efficiency—it can be used for scaling “eye candy”. For example, on a low-end rig, maybe a player just experiences a vanilla version of the gameplay. If our system is detecting idle frames, however, we can use it to add additional particles, graphic events, and other atmospheric tweaks to give the game a little more panache.
To implement this, use the functionality available in your favorite engine, framework, or language to gauge how much of the CPU is being used. Set flags within your code that make it easy to check how much “extra” processing power is available, and then set up your sub-systems to look at this flag and behave accordingly.
Chaining Optimizations Together
By combining these methods, it’s possible to make your code significantly more efficient. With that efficiency comes the ability to add more features, run on more systems, and ensure a more solid experience for the players.
Do you have any easy to implement code optimizations that you use regularly? Let me know about them!
No comments:
Post a Comment