Unity Performance Introduction

Maxi Vallejos
etermax technology
Published in
8 min readFeb 28, 2019

--

This is not the typical Unity guide where you find a list of things you must or must not do. I will take this list, wich you can find in Unity Documentation and tell you what we, developers, as members of a multidisciplinary video game team, can do about each item.

I’ve been working with Unity for 9 years, almost 7 of them specifically developing mobile games. Considering this target, there is this huge challenge, “to create a good looking game that runs smoothly in all current Android and iOS devices”.

Lately, cell phones are shipped with big screens with high resolutions, actually most of them have greater resolutions than most TVs used to play console games, but, these devices don’t have the same CPU power of a console neither the memory capacity. So when we are developing the game, we need to use some tricks to make things looks better , with less CPU power and memory usage.

Render Performance

In our scene, we are going to have many GameObjects with render components, for a character, enemies, obstacles, proyectiles, scenario, etc. Each of them will have an impact in the game performance. A renderer object has an associated material used by Unity to draw the said object. This action is named drawcall. The general idea is to have the least possible number of materials to draw, of course, we don’t want to cut the number of objects in our game.

Lucky for us, Unity has two ways of helping with this. Static and Dynamic Batching. If we follow some rules, Unity will take all the materials we have, group those that are the same, and draw them like one.

Static Batching

It applies best for backgrounds and scenes. The ideal thing to do is to combine objects that always appear together into a bigger object, and making an atlas with the texture of each of them. However, when combining them is not an option, Unity can group the objects with the same material in one drawing step by activating the “Static Batching” option.

To be clear, the cost of drawing the objects is much higher than the overhead that varies as per the number of triangles that form them. In conclusion, it is much more worthwhile to organize the objects strategically than to make them simple and low quality (low poly).

Dynamic Batching

For Unity to be able to apply “Dynamic Batching”, the object must not surpass 900 vertex, must not be scaled and, of course, that they use the same material. And by “the same material” we refer to the same instance.

There are some exceptions: the multi-pass shaders prevent the dynamic batching.

This is very practical for small objects that do not require large textures, so we can combine them in just one atlas*, and we can have a gun and several projectiles instantiated in a given moment that would only take up one drawcall.

In real time computer graphics, a texture atlas (also called a sprite sheet or an image sprite) is an image containing a collection of smaller images, usually packed together to reduce the atlas size — Wikipedia

Setup Assets

Audio Clips

As far as sounds go, there is not much to say. The rule of thumb is to configure every clip according to its length and use it will be given.

  • Compressed in memory: Short clips that must be synched with something visual to avoid the delay it would produce to load it in the moment.
  • Streaming: In order to prevent long sounds such as music or ambiance from taking up memory all the time, especially if it is not a clip that is frequently played.
  • Decompress on load: An exception for long and frequently used clips. It is best to use them as “decompress on load” so as to avoid the decompressing delay that might cause minor performance issues.

Textures

It is ALWAYS advisable to go to the advanced import menu for each texture that will be added to the project.

  • MaxSize: The most important thing is to choose the right size of the texture according to the function it has in our app. Larger textures take up more memories, and if they are made for an object that looks too small, they are not worth it.
  • Use Mip Map: Unity recommends using mipmaps because it helps load the textures faster and it enhances the performance when there are many visible objects in the scene. But it has a disadvantage, because in order to do this, it creates more smaller textures to use them progressively, thus enlarging the final size of the app. Therefore, it would also be advisable for those textures used on 3D objects that can be close or far from the camera.

Sprites

When the textures is used for 2D objects in the canvas of Unity, we usually use some techniques to reduce the size of the atlas. Generally it’s all about using small sprites and use them in different ways to get the final image.

It consists of using half the image as an asset. It can only be used if the complete image is symmetrical. Then, half the total size is saved in the atlas and it is drawn twice as “mirror” in order to obtain the complete image.

It can also be done with 1/4 image if the image is symmetrically both vertically and horizontally.

Also called “9-slicing”, it is used to re-scale vector images. A small image is saved in the atlas. When it is scaled upwards, the outcome is as if a larger image is being used.

It can also be configured so as not to stretch the borders of an image, i.e.: so that the rounded off corners of a rectangle are not deformed.

Perhaps the simpler one, similar to slice, it is used to create large images that are formed by a pattern of a smaller image.

Masks are used to shape a sprite. This way, different shapes can be drawn with a couple of small sprites. For example, using the mask of a rectangle and a simple slice from a color sprite underneath, or reusing another sprite that is used whole at another time, thus avoiding having to use it more than once.

It is very important not to overuse them and use them for every sprite in the app. The benefit is proportional to the size of the sprite: the smaller the sprite, the smaller the benefit. They all have, to a lesser or greater degree, a process overhead. Therefore, there would be no performance loss. However, when dealing with larger sprites such as backgrounds, which are used in different places and with some variables, there would be a trade of performance for less memory usage and app size.

Scripting

Don’t put everything in Update(), it is the most common mistake when someone is starting with Unity. Just leave the behaviour that will really change in time. Additionally, you could disable the update for those objects outside the camera render or far away.

Another common mistake is leaving the Update() empty, this will add unwanted overhead, even worse if you create a base class with this method and make many classes inherit from it.

Avoid the overuse of getComponent calls or GameObject find, they are expensive. When you want to refer an object in the inspector, try using the specific component instead of having a GameObject.

Calling your methods only when you need it will be better. This way, you will save many resources. One way to do this is using Events or Actions, but this principle perfectly applies to Reactive Programming, so you can subscribe to any change you want to react and do something. If you are interested in Reactive Programming in Unity, you can check out UniRX.

Bad using of GetComponent

Better using of GetComponent

What did I do wrong?

Avoid premature optimization

You follow all Unity guides and tips, but at the end your application is having problems. One common mistake is premature optimization, you probably create every sprite to be mirrored or sliced, but this not necessarily is a good thing. If you have hundreds of tiny sprites at the same time, you are adding more CPU overhead than the memory and atlas size you are gaining. Sprites optimization are better for big sprites, so the gain is considerably bigger than the CPU overhead.

The same applies to prefabs composition. Maybe you split one big prefab into many smaller trying to reuse them and avoiding increase your app size. But if you have to instantiate many composed prefabs every time, you can improve the instatiantion by having the original big prefab and resigning to have a slightly bigger app size.

Profiling

How can I know what I am doing wrong? Using Unity Profiler.

With this tool, you can find what is taking too long in a frame, what causes a spike, how much memory is the application using. The most important thing is to run the Profiler on an app on the phone instead of in the Unity editor, because the hardware changes between platforms and that can modify the results.

Stop the game at any moment to see how much memory it is using, and analyze what is using it.

You can see if the draw calls are what you were expecting, otherwise, you can pause the application and find which sprites are being used and how many references they have.

If you found spikes in the CPU usage graph and the fps drop significantly, there is something that is taking too long and you should inspect it.

The possible reasons are:

  • Expensive algorithms: This types of spikes are usually the easiest to fix, if you have expensive operation and can not be optimized, consider moving them to coroutines or to another thread.
  • Memory allocation: Two words: “Garbage Collection”. The memory allocated in your code must be released at some point after using it. When it happens, the application will freeze for a fraction of a second. The only thing you can do is avoiding garbage collection by reducing the memory allocated by your code.
  • GameObjects instantiation: Instantiating GameObjects is expensive, especially if it contains many children and components. Try to instantiate the heavy GameObjects before and hide them until needed. Consider using pooling of GameObjects you are going to create and destroying repeatedly.

--

--