JRL About

Basic Game Optimization Techniques

January 9, 2017
Updated: November 30, 2021

Twitter Hacker News Reddit LinkedIn
R

ecently I made a simple game and was surprised to run into performance issues. How to fix it? I could invest my time in micro optimizations (JS), intensive graphical analysis (GTAV and DOOM), or conflicting forum advice. Eventually I discovered the issue, and also collected some juicy optimization tips. I hope it helps other devs. I'll start with broad advice, and narrow down to generally applicable programming patterns.

The key to making programs fast is to make them do practically nothing. ;-) - Mike Haertel

Profilers

Profilers are tools that track a program's usage of the CPU, GPU, memory allocation, frame rate, and other key metrics. They are absolutely essential. Software does not run in a vaccum, so sometimes your game's slowdown could be caused by Adobe in the background. When optimizing you first need to identify the problem, and then you can begin comprehending it. Profilers allow you to start that journey.

Since my game was written with Phaser and Javascript I used Chrome's Timeline tool to profile. Below you can see the output of a short run of my game. The top section has labelled graphs of performance, mid-section has time taken to run specific functions, and the lower section has heap performance and summary. Other profilers act similarly, and by using them to find areas of poor performance you can immediately see the code causing it and start hunting down optimization issues.

Chrome Timeline in Birdu game
'Birdu' before and after I re-optimized for this article. Notice in the 'before' memory must be cleaned up more frequently, the CPU does more work, and there are more frame drops.

Use the latest and greatest

Engines are constantly getting better, make sure yours is up-to-date.

Do some research into plugins and tools that can accompany your engine. Stuff won't always be available, helpful, or as significant as advertised, but I feel I have to include this because incoporating Crosswalk into my hybrid app dramatically improved game performance.

Input/Output and Graphics

Between levels of the memory hierarchy there are orders of magnitude differences in the time it takes to access data, so be sure you are using memory closer to the top. Performance is commonly an aspect of data, not code.

Memory Hierarchy
Source

As a game programmer a lot of memory interaction is usually abstracted away from you by your engine and language of choice, but there is still plenty that you can control.

First off, having a garbage collector doesn't free you from needing to assiduously manage your memory. Each time the GC runs, it will eat up a lot of CPU resources. Ensure you only allocate what you need, and be sure to reuse as much as possible (covered more in the 'Heroes Never Die' section). Next, ensure you don't run out of memory by using smaller assets, or less of them. Trying to load huge images, sounds, and files into memory will may cause viable RAM to fill up, leading to abysmal performance. Using tiny or poor quality assets will make your game look bad, so work on this only if it becomes a problem. Ensure that the assets are optimized. Using a texture atlas or spritesheet over individual images will dramatically lower GPU draw calls and improve 2D performance. Using 3D objects with less vertices or polygons will make collision detection and rendering many times faster. Watch out for asset decompression. For example, in my game Birdu, mp3's induced a noticeable lag when starting while wav's did not. There was less decompression required. Next, reduce game world size to lower the amount that needs rendered and loaded at once. Only render what the user can see and load more of the game as needed. Try to improve data locality. Data that is used often will be loaded into the cache where it can accessed much more quickly. Read this article to find out how. Locality can be difficult to diagnosis and solve, so keep it in mind and recognise it may be causing issues when checking your profiler. Poor locality leads to cache misses, which can be abysmal for performance.

GPU and graphics optimizations can get super complicated and in-depth, so there is much more that you can optimize. For most non-AAA games the above advice should be sufficient, but I will try to keep it updated as I learn more.

Leverage Asynchronous Actions

Synchonous operations execute one-after-another, while asynchronous operations can be executed simultaneously. Most programs run synchronously. Asynchronous programs utilize threads to start a new task and still allow the calling code to keep running.

Asynchonous programming can be difficult to manage, but can greatly improve performance in certain tasks. The tasks need to be capable of parallelization, e.g. rendering, I/O, enemy AI, pathfinding and more. Once you start trying to have your program run simultaneously with itself, you need to watch out for a myriad of issues: data races, deadlocks, live locks, synchronization and increased complexity. Multi-threading can be hard, you should be prepared. I have not utilized multithreading in any games yet, but my understanding is that many game engines have a "job"-like architecture of multi-threading. Break tasks into atomic units (with dependencies on other units), and submit them all to a queue which will dispatch it to whatever worker thread is available. You typically do not want to create a thread per task, or try to manage everything on your own, as creating threads and having too many at once have performance tradeoffs.

Network calls should be used asynchronously. Most commonly developers will wait to use network calls until the user clicks on something, and then will make the user wait for it to return. Fetch this data early, and don't make people wait. I've also seen games that have multiple loading screens, one for making server calls and another to load assets/images into RAM (preloading). Combine those! This is an easy way to "optimize" the user experience.

Algorithm Analysis

Meat-and-potatoes CSE: know your big-O notation, algorithmic complexities, and be extremely wary of loops. Learn to love hashmaps. Nearest neighbor computation and pathfinding are common tasks in games that can dramatically benefit from data structures or better algorithm design.

Focus on Reoccurring Tasks

A lot of game programming occurs in the main update loop, or the physics loop. Even a small performance hit can make an impact when it is occurring 60 times per second, potentially on many different objects. So use a keen eye when examining your game loops, frequently occurring enemies, or common tasks. A profiler can give more precise info on long-running functions, but if there is a general slowness to your game, this is a great place to start.

Sometimes, update code could go into a timer. Instead of running 60 times per second, a task could run once every few seconds, or even once a second. Similarly, code does not always need to run in every object instance. You might be able to do a calculation once, and use the result to update all individuals (similar to how Planet Coaster optimized their crowd mechanics). Also, be sure to cache (store) the output of expensive functions for subsequent use. It allows you to reuse the stored variable instead of using the offending function again.

Heroes Never Die

An extremely fundamental game optimization technique is recycling objects. Creating new objects is expensive: memory needs to be found and initialization code has to be traversed throughout the entire stack. Once you're done with that object, if you destroy its references the garbage collector will cause a performance hit when the GC cleans it up. If you don't destroy the references, but are done using it, GC will ignore it and you've created a memory leak.

Recycle your objects! Do not create nor destory dynamically. Instead, on game start pre-allocate your objects: enemies, bullets, etc, and disable them. Don't just stop at sprites: be sure to enable animations, tweens, text, annonymous functions…really any commonly created objects for reuse. Once you need a new one, grab a disabled/dead one and revive it: make it visible, reassign it to a new parent, start its update loop, set a new position, etc. This is called resource pooling. Once an object "dies", re-disable it and release it back to the pool. This will keep object allocation and deallocation snappy, despite your game's many interacting pieces.

Finding Neighbors

Finding an object's nearest neighbor can be a very expensive task. By default, objects have no ordering. So, to find a neighbor, you need to loop over all objects, calculate distance metrics, and then find the closest. If you have many objects, then leveraging spatial locality could help.

Keep an array of the items sorted by their distance to your player. This is a time-to-space tradeoff, but is usually worth it. Of course, moving objects will now have to update their position, so perform measurements to avoid a negative overall impact on CPU performance. You can change your array to a grid or other data structure to get even greater performance gains. Check out this article for more information.

And beyond!

This guide is by no means comprehensive, but should be enough to get you started. Be sure to read documentation and perform research to find engine-specific functions or problem-specific research that can help improve performance.

Be wary of pre-optimizing code and over-optimizing code. As a developer, your primary goal is to ship your project. As an engineer, you need to be aware of the value of your time, tradeoffs of maintainable and optimized code, and the risk of unintented consequences when modifying code. Try to program with a mind towards optimization, but refrain from making significant changes to existing systems until a verifiable problem arises.

Thanks for stopping by!