The problem
Sometimes we have Update methods in our Unity gameobjects that only run if a certain logical condition is true. Usually that is not a performance bottleneck. A single if statement in one gameobject is not going to be much of a problem.
The real problem arises when we have many gameobjects that have the same script, that runs our update method, or have different scripts with different update methods that only run if a certain condition is true. Having tens or sometimes hundreds of updates that all calculate the same condition every frame is expensive. Especially if we consider that unless that expression is true, nothing will be executed in our Update method.
Expensive if statements
There are two main problems that arise with Unity’s Update method:
- When something happens then we want our Update methods to start executing and when something else happens to stop executing or
- As long a logical expression is true our Update methods execute.
An example for the first case could be that whenever we press a button then our Update methods will start executing and whenever another button is pressed they stop. For the second case, any if statement that is somewhat expensive and gets calculated at the start of our update method and returns immediately if false will do.
Again that is not a problem for one object, but if many objects use that expression and only then execute code in their update methods (which may be different code for each script), can create an unnecessary performance bottleneck.
A naive solution
A simple solution that someone may think, is to have another script calculate the expression every update and depending on the result have a boolean variable become true or false. Then the update methods check that boolean variable and execute only if that boolean is true.
This creates two problems:
- Our update methods still execute even if only to check that variable and then exit immediately. This slows down our program because of the context change. Our code, still has to move from the C# space to the C++ space and that takes time. Not a lot of time for a small amount of scripts, but for many scripts, this delay becomes a consideration.
- Most importantly, we risk to have a race condition. Nothing guarantees that the calculation of our expression in the update of our script will execute before all other updates. This can lead from small problems, like our updates won’t run for a frame or will stop running one frame later, to huge problems like our code crashing, if the expression has a null check.
Let’s see a way to solve this and avoid the above two problems:
Creating an Update Manager
Instead of having update methods in our scripts, we can have normal methods that only contain the code that we want to execute. Then we create a script (let’s call it UpdateManager) that has an Update method that checks the condition we want and if true, calls those methods.
Now we calculate the expensive logical expression once and if true, only then we call the methods that have the code that we want executed.
But this is a maintenance nightmare. Our UpdateManager that calculates the logical expression must have a dependency to every script that wants the result:
Every time that we create a new script, we have to go and find the piece of code that has the relevant expression and add it, while creating a new dependency to a low level policy.
Every time we remove one of our scripts because we don’t need it, we have to go again to our UpdateManager and remove the line that calls the method of this script.
Finally every time we have different conditions and want to change the condition that will allow our method to be executed, we have to do both. Find the piece of code in our UpdateManager, remove it and add it again in the place of our code where the other condition is calculated.
Because we have many scripts, our UpdateManager will become a huge mess, that is difficult to change, does not obey the open/closed principle and eventually will be a god object.
Let’s solve that by reversing our dependencies.
Reversing the dependency
The first thing we have to do, is to create an abstraction:
public interface IMyUpdate
{
void MyUpdate();
}
Whenever we have a script, that wants to run code in the update method, we will have it implement the IMyUpdate
interface and add that code in the MyUpdate
method.
Then we will have in our UpdateManager, collections that can contain those IMyUpdate
types. Those collections will execute each script’s MyUpdate
methods only if our condition is true. A code example will explain it better:
public class UpdateManager : MonoBehaviour
{
private readonly HashSet<IMyUpdate> myMod3Updates = new();
public void AddMod3Updatable(IMyUpdate myUpdate) => myMod3Updates.Add(myUpdate);
public void RemoveMod3Updatable(IMyUpdate myUpdate) => myMod3Updates.Remove(myUpdate);
private void RunMod3Updates()
{
if (Time.frameCount % 3 == 0)
{
using var e = myMod3Updates.GetEnumerator();
while (e.MoveNext())
e.Current?.MyUpdate();
}
}
private void Update()
{
RunMod3Updates();
}
}
In this example, every script that registers in the UpdateManager
will have its MyUpdate
code executed only every three frames.
The mod check is not expensive at all, but the if statement in the code could contain anything and no matter how many scripts need that check, it will only happen once every update.
Even if the check is cheap, this is useful for easily changing many scripts’ update conditions at once. If we decide that we want our code to run every four frames for those scripts, we don’t need to go and change all those scripts but we only need to change the condition in our UpdateManager
.
An example of how a script can use our UpdateManager
:
public class MyMod3Update : MonoBehaviour, IMyUpdate
{
[SerializeField] private UpdateManager updateManager;
private void OnEnable() => updateManager.AddMod3Updatable(this);
void IMyUpdate.MyUpdate()
{
// our update code here
}
private void OnDisable() => updateManager.RemoveMod3Updatable(this);
}
Different methods for different conditions
Our UpdateManager
doesn’t need to have only one collection with one condition and our scripts don’t have to register and unregister only in OnEnable
and OnDisable
. For example our UpdateManager
could be like this:
public class UpdateManager : MonoBehaviour
{
private readonly HashSet<IMyUpdate> myUpdates = new();
private readonly HashSet<IMyUpdate> myMod3Updates = new();
public void AddMod4Updatable(IMyUpdate myUpdate) => myUpdates.Add(myUpdate);
public void RemoveMod4Updatable(IMyUpdate myUpdate) => myUpdates.Remove(myUpdate);
public void AddMod3Updatable(IMyUpdate myUpdate) => myMod3Updates.Add(myUpdate);
public void RemoveMod3Updatable(IMyUpdate myUpdate) => myMod3Updates.Remove(myUpdate);
private void RunMod4Updates()
{
if (Time.frameCount % 4 == 0)
{
using var e = myUpdates.GetEnumerator();
while (e.MoveNext())
e.Current?.MyUpdate();
}
}
private void RunMod3Updates()
{
if (Time.frameCount % 3 == 0)
{
using var e = myMod3Updates.GetEnumerator();
while (e.MoveNext())
e.Current?.MyUpdate();
}
}
private void Update()
{
RunMod4Updates();
RunMod3Updates();
}
}
Now everything can register with the AddMod3Updatable
or the AddMod4Updatable
and its method will run when the respective condition is true.
Also a script can register/unregister only when something happens:
public class WithMyUpdate : MonoBehaviour, IMyUpdate
{
[SerializeField] private UpdateManager updateManager;
void IMyUpdate.MyUpdate()
{
// our update code here
}
private void OnDisable() => updateManager.RemoveMod4Updatable(this);
public void StartRunningUpdate() => updateManager.AddMod4Updatable(this);
public void StopRunningUpdate() => updateManager.RemoveMod4Updatable(this);
}
Here we can call the StartRunningUpdate
and StopRunningUpdate
methods whenever we want, for example when a button is pressed. Until then, no code will run in the C++ space of the update method.
Conclusion
I hope you found this post useful, not only for the performance benefits, but also for an easy way to have a lot of scripts run whenever a condition is true and still have an easy way to change that condition, by changing only one line of code and not having to go and change every script.
Performance is a big topic, but depends on context, so in my next post I will show a script I have made that displays how expensive really are, things that are considered expensive in Unity, and comparisons in performance between those things.
Until then, thank you for reading and if you think I forgot something or if you have any questions or comments, you can use the comments section or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.