2D Mobile Game Performance in Unity Part 2

In Part 1 we talked about minimizing allocations and deallocations via upfront loading and object pooling. When allocations are needed, which they inevitably will be, we looked at using prefabs from the Resources folder to ensure that underlying assets can be selectively loaded and unloaded from memory.

Sprite Packing and Image Settings
In most cases, the biggest drain on memory both in a scene and when creating our final installation will be image assets. For starters (and this is obvious), we’ll keep our image assets small (tile and recycle whenever we have the chance), remove assets when possible (does our character run animation loop need 15 frames or can it be done in 8?), and crop them as tightly as possible (seriously, I’ve had artists hand me more assets with oceans of empty, transparent space than I can count). If we have Unity Pro, then we also have access to the Unity sprite packer (be aware that we can only use the sprite packer with Android or iOS if we also have Android and iOS Pro licenses). Sprite packing will bundle our individual image assets into one or more sprite atlases. This is critical for a mobile release because sprite packing allows image compression to be used on our imported image assets. In turn, this will greatly reduce the memory burden of our installed application and individual scenes.

To enable sprite packing, set a Packing Tag in the inspector for each texture asset. The Packing Tag will specify that the asset will be sprite packed into an atlas with the Packing Tag name. Also pay attention to the Max Size and Compression values in the inspector. We’ll typically want to change the compression setting from “True Color” to “Compressed” so that the final assets in the sprite atlas are compressed when the project is built and deployed. We can also adjust the Max Size value. Reducing the Max Size of a texture so that is smaller than the original asset will cause the asset to be scaled down and hence decreased in size. Obviously large textures require more memory and are slower to display so reducing the Max Size of a texture will reduce the memory burden and increase performance. Be warned, changing the compression and the max texture size can have a significant impact on the asset quality (and this will vary depending on the device that the project is deployed to). Reaching a state where project assets are optimized for performance but still retain high levels of quality is a careful balancing act. This often requires several patient iterations of tweak > build > deploy before reaching a final solution.

It’s also important to carefully inspect which assets are packed together. If done haphazardly, sprite packing can increase our overall memory usage. When an asset is needed in a scene, the entire sprite atlas will need to be loaded into memory to access the required asset. Pack assets together that will be used together in one or more scenes. For example, if we have two enemy classes, dinosaurs and robots, that are never used in the same scene, we’ll typically want to pack the dinosaur and robot assets into different atlases. Doing so will prevent loading both sets of assets when only one set is needed. When setting the Packing Tag on each assets, we’ll set PACKROBOT for all of the robot assets and PACKDINOSAUR for all of the dinosaur assets. This will results in 2 separate sprite atlases that can be unpacked independent of one another and only when needed.

Reduce Script Object References
In Unity, scripts and other objects are typically attached to GameObjects, and at some point during our game we may need to access those object using a GetComponent call like so:
GameObject.GetComponent().DoFancyThing();
However, GetComponent is an expensive call so use it as sparingly as possible. Whenever possible make a local variable that stores a reference to scripts and other objects attached to a GameObject. Do not repeatedly call GetComponent in an Update function.

Cut Down Superfluous Updates
In all software development, cutting down on superfluous code is important, but this is magnified in mobile game development where performance easily degrades. Carefully scrutinize the update loop and look for any redundant or unnecessary calls and remove them. One common culprit is unnecessary string calls or text mesh updates. For example, we might be tempted to update our game score in an update loop as follows:

This works, but it’s sloppy. Is the user score really changing at *each* iteration of the game loop? If not, then updating a text mesh 30-60 times a second is wasteful. The performance loss is tiny, but when our code is littered with these sort of issues, a handful of seemingly innocuous code fragments suddenly turn into legitimate performance problems. In our case, we can quickly add in a local variable that keeps track of the score, and only make an update to the text mesh when the score actually changes. The equality call is faster than updating the text mesh and creates a smaller performance hit when called within each iteration of the game loop.

Add an Empty Loading Scene
When a new scene is loaded, all of the assets from the previous scene are destroyed and eventually garbage collected. However, this process does not occur synchronously. We may in fact see a brief spike in memory usage as the previous scene’s assets are being unloaded and the new scene’s assets are simultaneously being loaded. In the worst cases, this memory spike may cause an out of memory error and application crash. One trick to help avoid this is to add an empty loading scene to ensure that the assets from scene A are completely unloaded before the assets from scene B are loaded. So instead of loading scenes in this order:
Scene 1 > Scene 2
Load scenes in this order:
Scene 1 > Empty Loading Scene > Scene 2
This allows the assets from Scene 1 to be unloaded while an asset-less scene is created. Similarly, the assets from Scene 2 will be loaded while an empty, asset-less scene is tossed from memory. This extra protection will help reduce fatal memory spikes.

Remove Debug Log Statements
Yes, debug log statements slow things down. Be sure to remove debug log statements before deploying a build to an actual device. To simplify things, we can always wrap debug log statements in our own function that we can turn on or off with a Global or Compiler flag.

There are dozens of other methods and tricks that can be used to increase mobile performance, but hopefully these tips will serve as a helpful introduction for beginning mobile developers. With a little luck, your mobile game will be ready to roll in the App Store and on Google Play in no time!