Memory frequently gets the least attention when optimizing video games, especially when it comes to indie games, which are usually smaller in scope. Saleblazers, however, is a very big game. Long load times, lag spikes when spawning, blue screens, and bloat can become major memory issues.
Light games that can run on lower end machines and can be ported onto consoles are playable by the widest audience. While the average PC or laptop has a decent 8GB of RAM to work with, typical consoles and mobile devices are constrained by having only 0.5GB to 4GB of RAM. Failing to optimize cpu or gpu can make a game feel unplayable due to lagginess. Failing to optimize for memory, however, can make a game actually unplayable. Through hard work and perseverance, the Airstrafe team worked together to optimize Saleblazers and successfully reduced memory usage by 75%, from ~10GB to ~2.5GB.
While optimizing for memory is a straightforward concept–just remove what’s unnecessary–the devil is in the details of sussing out what can be cut.
Programmer Kenny Doan led the memory optimization charge and provides insight on a few things to look for when optimizing for memory:
Textures are the number one culprit of memory problems in 3D games. Most assets have more than one texture, which is problematic because a single 1024×1024 or 1K texture can take up ~700 KB of memory. In comparison, a 100,000 word document takes up ~600 KB of memory. The entire narrative of a game can take up less memory than a single texture.
Reduce Texture Resolution
Reducing texture resolution can majorly reduce memory usage without sacrificing how good assets look to the human eye if the asset only takes up a small portion of the screen and is farther in distance from the player.
Grass is a prime example of where the textures can be the lowest.
While the left image uses 2K textures and takes up a whopping 5.3MB of memory and the right image uses 128×128 and only takes up a paltry 0.2MB of memory, it will be hard for most players to tell the difference between the two images.
Reducing texture resolution, however, is not a universal solution to memory issues. For assets that take up a larger part of the screen and are closer in distance to the player, artistic discretion is needed to weigh the pros and cons of reducing fidelity in favor of memory improvements.
The left image uses a 1K texture and takes up 1.3MB of memory. The right image uses 512×512 and takes up 0.3MB of memory. Hard decisions need to be made because the difference is discernible to the player.
A small, unremarkable cup probably doesn’t deserve a 2K texture. A large tree ready for closeups probably deserves higher resolution textures. It is also important to consider that most players would run low graphics if it meant hitting that sweet 60 fps.
Not having any textures at all is also an option. While it is nice to have albedo texture for color, is it absolutely necessary to have that normal, metallic, height, or AO map? A 1K metalitic map costs the same as a 1K albedo, but the albedo is doing 80% of the work. Sometimes an asset does not even need the texture at all. For example, a wood log does not need a metallic map, and a smooth granite countertop does not need to have a normal.
The left image has a normal map and the right image does not. From a distance, they are indistinguishable to most players.
For games with many songs and long clips, audio can be a major contributor to memory issues. There are two categories of audio that can be optimized: short clips (anything less than 1 second) and long clips (more than 5 seconds). Clips between 1 and 5 seconds long are classified according to the developer’s discretion.
It is a cardinal sin to put long clips into the game without compression. .WAV files are the most common uncompressed audio type and can get as big as 200MB.
A 45 MB .WAV file can be compressed into a 4.5 MB MP3 file with very few players noticing unless they are playing with high end gear or have perfect hearing. A 30 second clip of heavy rain does not need to have mega-crisp quality. An easy way to reduce memory is to compress long clips as much as possible before noticeable distortion.
We can further reduce clip quality through Unity’s import to further reduce memory usage, which also makes loading audio files faster and prevents frame stutters.
Compressing short clips is very dependent on the SFX. If the clip is sharp and quick and has very specific audio, compressing the audio can mess up the sound itself. In this case, it may be a good idea to reduce the number of SFX–e.g. dynamically changing the pitch or volume in runtime through code versus making 10 versions of an SFX.
Although very dependent on the codebase, the general rule for code is: if it isn’t used, then it shouldn’t be there.
Saleblazers is an open world game. For Saleblazers to run smoothly, the game needs to load things in when the player is nearby versus loading everything all at once. Loading in objects can be slow if these objects are complex and have many assets. Long load times when moving from chunk to chunk can be seen as lag spikes to players, and having many “heavy” game objects active at the same time uses up a lot of memory.
Optimizing code by “faking” the trees in Saleblazers significantly reduced memory usage. Most trees do not need 80 percent of what is on them until a player needs to use those items, so we programmed trees to be inert objects that had the same visuals and colliders, but nothing else. Trees only become “real” when players interact with them or cut them down.
The image below shows the fake tree (PF_Tree_Bamboo_Large_Fake_On_Hit) object hierarchy on the top, and the real tree on the bottom (PF_Tree_Bamboo_Large) at the 7th line. Even without seeing the script reduction, there are many objects and components that were reduced. In addition to reducing memory usage, “faking” the trees significantly improved the game experience by reducing chunk load times from 320MS to 60MS.
Optimizing code to reduce memory fragmentation also helps to reduce memory usage. Memory fragmentation is caused by allocating memory with spawning objects or classes and then deleting them. For example, when an object of X size is deleted, it will leave a gap in memory that is X size. If a new object or more memory is allocated that is ¾X size, then there will be a ¼X size gap in memory. Unless there is an object that is ¼X size, the gap will effectively sit there and “use” memory. The general rule of thumb is to avoid creating gaps as much as possible by reducing the amount of allocations and destroys, which can be done by opting to pool objects and caching often-used lists or structs. Here is a cache example:
Instead of making a new list of game objects in every frame, use the same list and clear it. It is not being destroyed by the garbage collector and not allocated every frame, preventing fragmentation.
There are many other code-specific optimizations available, depending on the situation.
Using fewer shaders, less complex shaders, and reusing shaders are the best ways to reduce memory usage. Shaders have the potential to be either the least worrying or the most destructive thing about a game project.
For many indie developers, getting standard, widely-used shader assets is more efficient than making them from scratch. In Saleblazers, we used premade grass shaders. Although we only used parts of the shader we initially chose, the rest was still in memory. The grass shader alone used an absurd 200MB+ of memory. Unsurprisingly, 3rd party assets primarily focus on content, versatility, and performance versus memory usage. When we discovered how much memory our old grass shader used, we switched to a more lightweight shader that accomplished the same objectives while using only 36MB of memory.
While it may not be top of mind in the initial stages of game development when memory isn’t a big problem, thinking ahead about all specifications of an asset before it is used can save developers from big headaches down the road.
Reducing references requires the most knowledge of the game to do. Essentially, if something can exist in the game or a scene, then it exists in memory. If there is a character in a scene and it has references to other objects in the project, then those objects are all loaded in memory as well, even if they are not present in the scene. If a character spawns an apple through a reference on that object, then that apple and all of its contents are always in memory. If that apple also has a reference to a worm, then that worm is also in memory…and so on and so forth, quickly increasing memory usage. It is impractical to solve this problem by removing references because the apple and worm would then never exist in the first place.
There are two approaches to reducing references. The first approach is to split the game into scenes so everything in the game does not all have to be there at the same time. If the objects in one scene only exist in that scene alone, then those objects will only be in memory when that scene is active. Having levels in a game is preferable. In the case of Saleblazers, chunks in the open world are treated as “level” scenes. The example below has them all loaded at the same time:
The second, more complicated approach is to use addressables. Instead of using direct references to the objects and its relevant parts, you would reference an address that represents that asset. Unity usually preloads everything that is referenced, so spawning in it will be fast. This approach, however, requires things to be manually loading in that asset through a address. If done correctly, ther apple can be addressable and only load in when needed. This does come at the cost of runtime performance.
6. Do What Makes Sense
Memory encompasses many aspects of a game, and there is a limit to how much memory usage can be reduced. If something exists, then it is in the game memory. Figuring out what can be taken out of the game while staying true to the needs of the game is challenging and depends on its objectives and audience. If the game is meant to be a visual experience made for 4K, then it can be made for and targeted at players who have the best computers. Many developers of smaller scope games never need to worry about memory at all. When developing an expansive open world game like Saleblazers, however, memory is something to remain hyper-vigilant about.