Coding Optimizations for XR apps and game development in Unity

Akshay Arora
XRPractices
Published in
7 min readNov 29, 2022

In continuation to the last article regarding basics of optimizations in Unity, here are some script or code related best practices to be taken in consideration while making XR apps or games.

Getting a component

There’s 3 different ways to get a component:

  1. By giving the type: GetComponent<T>()
  2. By giving type as string: GetComponent(string)
  3. By giving “typeof” of the type: GetComponent(typeof(T))

So, which one to use?
It looks very obvious that the 1st method will be the most optimized one as it is the most common way, but it is not.

The fastest is 2nd method, Yes! the one with the string. In recent Unity versions, it is reengineered to run a lot quicker. It is followed by 1st method and then the slowest among these is the 3rd method.

You can also try it out and compare the time taken.

public class PerformanceComparison : MonoBehaviour
{
private const int NumberOfExecution = 1000;

private Transform Result { get; set; }

private void Start()
{
Method1();
Method2();
Method3();
}

private void Method1()
{
for (var i = 0; i < NumberOfExecution; i++)
{
Result = GetComponent<Transform>();
}
}

private void Method2()
{
for (var i = 0; i < NumberOfExecution; i++)
{
Result = (Transform) GetComponent("Transform");
}
}

private void Method3()
{
for (var i = 0; i < NumberOfExecution; i++)
{
Result = (Transform) GetComponent(typeof(Transform));
}
}
}

Apply Caching

public class CacheExample: MonoBehaviour
{
private const int NumberOfExecution = 1000;

private Transform Result { get; set; }

private void Start()
{
CacheExample();
}

private void CacheExample()
{
for (var i = 0; i < NumberOfExecution; i++)
{
Result = (Transform) GetComponent("Transform");
Result.Translate(1,1,1);
}
}
}

In the above example, we are trying to get the component every time within the for loop and translating it. It’s better to cache it before using it.

One way is to cache the transform in Start/OnEnable/Awake function, then reuse the same object.

public class CacheExample: MonoBehaviour
{
private const int NumberOfExecution = 1000;

private Transform Result { get; set; }

private void Start()
{
Result = (Transform) GetComponent("Transform");
CacheExample();
}

private void CacheExample()
{
for (var i = 0; i < NumberOfExecution; i++)
{
Result.Translate(1,1,1);
}
}
}

For few times, this way is fine but recommended way is to serialize the field.

public class CacheExample: MonoBehaviour
{
private const int NumberOfExecution = 1000;

[SerializeField] private Transform targetTransform;

private void Start()
{
CacheExample();
}

private void CacheExample()
{
for (var i = 0; i < NumberOfExecution; i++)
{
targetTransform.Translate(1,1,1);
}
}
}

NullReferenceException

This is the most basic exception that comes when we try to use something that is null. This means we set it to null, or at runtime the object sets to null, or we never set it to anything at all.

If we expect the reference sometimes to be null, we can check for it being null before accessing instance members:

public class NullCheckExample: MonoBehaviour
{
private const int NumberOfExecution = 1000;
[SerializeField] private Transform result;

private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Method1();
Method2();
}
}

private void Method1()
{
for (var i = 0; i < NumberOfExecution; i++)
{
if (result != null)
{
}
}
}

private void Method2()
{
for (var i = 0; i < NumberOfExecution; i++)
{
if (!ReferenceEquals(result, null))
{
}
}
}
}

Though Method1() is very common to use, Method2 is the fastest way to check.

Comparing Tag

We can compare tags by two ways:

public class CompareTag: MonoBehaviour
{
private const int NumberOfExecution = 1000;
private GameObject _gameObject;

private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Method1();
Method2();
}
}

private void Method1()
{
for (var i = 0; i < NumberOfExecution; i++)
{
if (_gameObject.tag == "Player")
{
}
}
}

private void Method2()
{
for (var i = 0; i < NumberOfExecution; i++)
{
if (_gameObject.CompareTag("Player"))
{
}
}
}
}

Method2 is better than Method1.

Please note that Null check & Tag comparison will only makes difference if we do the same operation thousands of times. For few instances, it's fine to use whatever way that we want to use.

Structures vs Classes

Structs are considerably faster than classes ONLY IF the instances of the types are small or short-lived. Rest in every other case, we should use classes.

Garbage Collection

public class GarbageExample: MonoBehaviour
{
private const int NumberOfExecution = 1000;

private void Update()
{
CreatePlayers();
DestroyPlayers();
}

private void CreatePlayer()
{
for (var i = 0; i < NumberOfExecution; i++)
{
Gameobject player = new Gameobject();
AddPlayer(player);
}
}

private void AddPlayer(Gameobject player)
{
player.AddComponent<Player>();
}


private void DestroyPlayer()
{
Gameobject[] players = GameObject.FindObjectOfType("Player");
for (var i = 0; i < NumberOfExecution; i++)
{
Destroy(players[i]);
}
}
}

To solve the garbage issue, lets understand the concept of memory storage in Stack & Heap. When we play the scene, any method inside the Update loop placed on the stack above it.

The execution order will be first Update() will run and will be on the bottom of the stack, then CreatePlayer() will get called and place over the Update() function in the stack. Then, CreatePlayer() will call AddPlayer() funtion which will be placed over CreatePlayer() function. So, the execution will be like:

Stack view after above operation

Once AddPlayer() call is done, it will be removed from the stack and the stack will look like:

Stack view after AddPlayer() is done

Once CreatePlayer() is done, it will be removed and replaced by DestroyPlayer()

Stack view after CreatePlayer() is finished and DestroyPlayer() called

Now, let's talk about what is happening within these calls. In case of CreatePlayer(), this is when the Heap comes into picture.

So, we are first creating variable i in the for loop and it will increase once the loop goes on. In the loop, we are creating new player. So, every time new player GameObject is created and going into the heap. At the end of the loop, we will have 1000 newly created will be sitting inside the Heap only.

Sample Heap view

It will be removed only when the process of Garbage Collection takes place and cleans it up.

Please note that Garbage collections only collects the unused things. As we know, gameobjects created will be there in the hierarchy window in Unity, and other processes will be accessing them so it will not be picked by Garbage Collection.

After the CreatePlayer() done executing, local variable i will get removed by the garbage collector but all the player gameobjects will be sitting there.

Now, when the DestroyPlayer() gets called, it will destroy all the players, but they will still be there in the heap until the garbage collector collects it automatically. This is a limitation in C#.

So, what’s the need of the above theory? This is to give heads up that we need to take care of the variables & objects that we create in such a way, that it should not leave “garbage” after the call which cannot be picked up by the automatic garbage collector. You can check the GC.Alloc in the profiler window to get an idea of how much memory is getting allocated for the garbage.

Object Pooling

Object Pooling is a way to manage the memory bit better and control the garbage collection. In the above example, we were creating 1000 players and destroying them at some point. As we discussed in the last section that these players will destroy not themselves until unless they went to the process of automatic garbage collection.

Consider a problem in which we need to create thousands of objects each frame and then destroy them. If we leave it to automatic garbage collection which will happen after fixed interval, it will create a lot of lags in the game. To fix this problem, we create pool of objects which can be reusable by the game and this pool can be destroyed at the end of the game saving lot of memory and garbage collection processing. If you want to know how we can implement Object Pooling and understand it in more details, you can refer to this blog by Unity: Introduction to Object Pooling — Unity Learn

Array vs List vs Dictionary

These data containers have differences in their usage and may be used in case-to-case situations. But if we compare them by some basic operation like adding the data, iterating, search & removing the data, we have some differences.

We mentioned rankings based on the performance we observed in the Unity profiler while running these operations over thousands of times.

  • Adding values in the data containers
    1st. Array
    2nd. List
    3rd. Dictionary
  • Iterating values in the data containers
    1st. Array
    2nd. Dictionary
    3rd. List
  • Searching values in the data containers
    1st. Array
    2nd. Dictionary
    3rd. List
  • Removing values in the data containers
    1st. Array
    2nd. Dictionary
    3rd. List

That is it for the code optimization. Will publish the optimizations related to graphics in couple of days.

Thanks!

--

--