Coding Optimizations for XR apps and game development in Unity
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:
- By giving the type:
GetComponent<T>()
- By giving type as string:
GetComponent(string)
- 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:
Once AddPlayer()
call is done, it will be removed from the stack and the stack will look like:
Once CreatePlayer()
is done, it will be removed and replaced by DestroyPlayer()
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.
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!