Basics of Optimization in Unity

Ali Emre Onur
6 min readJul 25, 2021

--

On my previous article on Unity Profiler (click here), I have provided some basic optimization examples. Before moving on, please do not forget that unused scripts and methods are bag of load for your project and you should eliminate them.

In this article, I will be presenting additional suggestions for optimizing your game mainly by scripting.

Caching

While caching an element, make sure to cache the one that you want to access to. For instance, if you want to change the color of a game object, you will need to access to its material via Renderer. However, if you cache the renderer, the game will still re-access the material on each call. If you analyze the process within the Profiler, you will notice that Unity Engine processes an additional GetComponent action — in order to reach to the component after the dot — which is the material in this case. Rather, it is more efficient to cache the element directly. Thus, make sure to cache the exact component you want to access to have a more optimized code. In this example, we need to cache the material — not the Renderer.

Material _material;void Start(){ _material = GetComponent<MeshRenderer>().material;}

New Objects

You probably know the fact that you need to prevent using too many statements with “new” keyword. On my article on “Memory Management In Unity”, I have underlined the importance of managing the heap memory in order to have minimum garbage collected within our project.

Long story short, the reference type variables that are created by using the “new” keyword creates a load on the heap memory. Since the value type variables do not cause any GC allocation, we shall look out for the cases that we can use a value type variable rather than a reference type. If not possible, object pooling must be used to prevent the Unity from destroying and recreating the objects again and again.

For instance; it may be possible to use a struct rather than a class in an appropriate scenario. As structs are value types, they are allocated within the Stack memory and managed by the OS ant they will not be resulting any garbage collection. An example is provided below:

Let’s initialize a color variable with new keyword and run the Unity Profiler to observe the allocated space on the GC.

Color aColor = new Color(); 

Even if you put it in a for loop, you will realize that it will create 0 B of GC since it is a value type. Likewise, instantiating a new Vector3 variable also do not cause any garbage — as it is also a value type (if you click on Vector3, you will observe that it is a type of struct — which is a value type).

Despite the fact that value types do not cause GC Allocation as reference type, redundant usage of value types in an aggresive way is still going to still slow down your game. For more detailed information, you can check my previous article by clicking here.

Coroutines

In my article on the Unity Profiler, I have presented the Coroutines example for unnecessary memory loading. This is because of the fact that WaitForSeconds is a class, which means that it is a reference type — being stored within the Heap. Each time we are using the “new” keyword within a coroutine, we are creating a new class — and a new memory allocation within the Heap as a result. It is best practice to cache these reference typed variables/objects to use the memory more efficiently. If you test the case of 2 coroutines: one with a cached WaitForSeconds variable and one with not-cached, you will notice that the cached one will not result in any data usage in the GC Alloc column. Despite it may look like a small benefit, I believe it’s worth the effort to optimize it. Here’s the example again that I have presented on my article on Unity Profiler:

Coroutine Example:

On the script above, especially the coroutine called many times during the gameplay — as it is a simple click delay. By creating a variable of type WaitForSeconds and caching it, we can eliminate the redundant object creating during runtime — resulting in eliminated garbage creation.

RayCasts

To prevent observing garbage collection as a result of Physics Raycast, we can use Physics.RaycastNonAlloc, which will result in no garbage collection. Here’s the link for Unity Manual:

Additionally, if you want to send a ray from the main camera, make sure to cache the main camera first — as main camera is also a reference type, which means it will create a new memory allocation each time we access it if we do not reference it.

Strings

Probably you have heard the concept of strings are slow in programming and you shall avoid using them unnecessarily. First of all, strings are reference types. Strings are actually an array of characters and the runtime engine has no idea about the length of a string until executing the line. As Unity explains:

Any manipulation of a string results in the allocation of a full new string. This is relatively expensive, and repeated string concatenations can develop into performance problems when performed on large strings, on large datasets, or in tight loops.

Thus, processing a string is actually costly and it also a source of garbage collection. It is crucial to eliminate the unnecessary usage of strings in our games.

On my article on the Unity Profiler, I have also mentioned to avoid using Debug.Log statements after making sure that the relevant field is working fine. Unity suggests usage of Debug.Logs with a conditional attribute.

Image Source: Unity Manual

Additional Optimizations

While reaching to a property within Animator, Material or Shader; the method with integer values rather than string values shall be preferred — as the Unity Engine accesses these properties also by integer.

Null checking in every frame or in loops shall be prevented as it may result in serious performance issues. For instance, if you want to check if the player game object is null before handling an action within the Update method, you may handle the null check within the start method.

Avoid the usage of GameObject.Find and GameObject.FindObjectOfType in coding, as these will search all of the gameobjects within the scene one by one. However, Unity suggests that Singleton objects are exception for this case, in which FindObjectOfType is acceptable. As a general suggestion, in the cases where you would not want to drag the game object within the inspector, it might be better to use GameObjectFindWithTag(“Tag”).

Aside from the information provided above, you can always try and test which methods are more efficient than another using the Profiler.

To ease up the debugging within the Unity profiler, we can create labels just like below:

On my next article, I will be focusing on optimization in UI.

--

--