Creating a custom Update() layer

Earlier in this chapter, in the Update, coroutines, and InvokeRepeating section, we discussed the relative pros and cons of using these Unity Engine features as a means of avoiding excessive CPU workload during most of our frames. Regardless of which of these approaches we might adopt, there is an additional risk of having lots of MonoBehaviours written to periodically call some function, which is having too many methods triggering in the same frame simultaneously.

Imagine thousands of MonoBehaviours that initialized together at the start of a scene, each starting a coroutine at the same time that will process their AI tasks every 500 milliseconds. It is highly likely that they would all trigger within the same frame, causing a huge spike in its CPU usage for a moment, which settles down temporarily and then spikes again a few moments later when the next round of AI processing is due. Ideally, we would want to spread these invocations out over time.

The following are the possible solutions to this problem:

  • Generating a random amount of time to wait each time the timer expires or a coroutine triggers
  • Spreading out coroutine initialization so that only a handful of them are started at each frame
  • Passing the responsibility of calling updates to some God class that places a limit on the number of invocations that occur each frame

The first two options are appealing since they're relatively simple and we know that coroutines can potentially save us a lot of unnecessary overhead. However, as we discussed, there are many dangers and unexpected side effects associated with such drastic design changes.

A potentially better approach to optimize updates is to not use Update() at all—or, more accurately, to use it only once. When Unity calls Update(), and in fact, any of its callbacks, it crosses the aforementioned Native-Managed Bridge, which can be a costly task. In other words, the processing cost of executing 1,000 separate Update() callbacks will be more expensive than executing one Update() callback, which calls into 1,000 regular functions. As we witnessed in the Remove empty callback definitions section, calling Update() thousands of times is not a trivial amount of work for the CPU to undertake, primarily because of the bridge. We can, therefore, minimize how often Unity needs to cross the bridge by having a God class MonoBehaviour use its own Update() callback to call our own custom update-style system used by our custom components.

In fact, many Unity developers prefer implementing this design right from the start of their projects, as it gives them finer control over when and how updates propagate throughout the system; this can be used for things such as menu pausing, cool-time manipulation effects, or prioritizing important tasks and/or suspending low-priority tasks if we detect that we're about to reach our CPU budget for the current frame.

All objects wanting to integrate with such a system must have a common entry point. We can achieve this through an Interface class with the interface keyword. An Interface is a code construct used to essentially set up a contract whereby any class that implements the Interface class must provide a specific series of methods. In other words, if we know the object implements an Interface class, then we can be certain about what methods are available. In C#, classes can only derive from a single base class, but they can implement any number of Interface classes (this avoids the deadly diamond of death problem that C++ programmers will be familiar with).

The following Interface class definition will suffice, which only requires the implementing class to define a single method called OnUpdate():

public interface IUpdateable {
void OnUpdate(float dt);
}
It's common practice to start an Interface class definition with an uppercase "I" to make it clear that it is an Interface class we're dealing with. The beauty of Interface classes is that they improve the decoupling of our code base, allowing huge subsystems to be replaced, and as long as the Interface class is adhered to, we will have greater confidence that it will continue to function as intended.

Next, we'll define a custom MonoBehaviour type that implements this Interface class:

public class UpdateableComponent : MonoBehaviour, IUpdateable {
public virtual void OnUpdate(float dt) {}
}

Note that we're naming the method OnUpdate() rather than Update(). We're defining a custom version of the same concept, but we want to avoid name collisions with the built-in Update() callback.

The OnUpdate() method of the UpdateableComponent class retrieves the current delta time (dt), which spares us from a bunch of unnecessary Time.deltaTime calls, which are commonly used in Update() callbacks. We've also created the  virtual function to allow derived classes to customize it.

This function will never be called as it's currently being written. Unity automatically grabs and invokes methods defined with the Update() name, but has no concept of our OnUpdate() function, so we will need to implement something that will call this method when the time is appropriate. For example, some kind of GameLogic God class could be used for this purpose.

During the initialization of this component, we should do something to notify our GameLogic object of both its existence and its destruction so that it knows when to start and stop calling its OnUpdate() function.

In the following example, we will assume that our GameLogic class is SingletonComponent, as defined earlier, in the Singleton components section, and has appropriate static functions defined for registration and deregistration. Bear in mind that it could just as easily use the aforementioned MessagingSystem to notify GameLogic of its creation/destruction.

For MonoBehaviours to hook into this system, the most appropriate place is within their Start() and OnDestroy() callbacks:

void Start() {
GameLogic.Instance.RegisterUpdateableObject(this);
}

void OnDestroy() {
if (GameLogic.Instance.IsAlive) {
GameLogic.Instance.DeregisterUpdateableObject(this);
}
}

It is best to use the Start() method for the task of registration, since using Start() means that we can be certain all other pre-existing components will have at least had their Awake() methods called prior to this moment. This way, any critical initialization work will have already been done on the object before we start invoking updates on it.

Note that because we're using Start() in a MonoBehaviour base class, if we define a Start() method in a derived class, it will effectively override the base class definition, and Unity will grab the derived Start() method as a callback instead. It would, therefore, be wise to implement a virtual Initialize() method so that derived classes can override it to customize initialization behavior without interfering with the base class's task of notifying the GameLogic object of our component's existence.

The following code provides an example of how we might implement a virtual Initialize() method:

void Start() {
GameLogic.Instance.RegisterUpdateableObject(this);
Initialize();
}

protected virtual void Initialize() {
// derived classes should override this method for initialization code, and NOT reimplement Start()
}

Finally, we will need to implement the GameLogic class. The implementation is effectively the same whether it is SingletonComponent or MonoBehaviour, and whether or not it uses MessagingSystem. Either way, our UpdateableComponent class must register and deregister as IUpdateable objects, and the GameLogic class must use its own Update() callback to iterate through every registered object and call their OnUpdate() function.

Here is the definition for our GameLogic class:

public class GameLogicSingletonComponent : SingletonComponent<GameLogicSingletonComponent> {
public static GameLogicSingletonComponent Instance {
get { return ((GameLogicSingletonComponent)_Instance); }
set { _Instance = value; }
}

List<IUpdateable> _updateableObjects = new List<IUpdateable>();

public void RegisterUpdateableObject(IUpdateable obj) {
if (!_updateableObjects.Contains(obj)) {
_updateableObjects.Add(obj);
}
}

public void DeregisterUpdateableObject(IUpdateable obj) {
if (_updateableObjects.Contains(obj)) {
_updateableObjects.Remove(obj);
}
}

void Update()
{
float dt = Time.deltaTime;
for (int i = 0; i < _updateableObjects.Count; ++i) {
_updateableObjects[i].OnUpdate(dt);
}
}
}

If we make sure that all of our custom components inherit from the UpdateableComponent class, then we've effectively replaced N invocations of the Update() callback with just one Update() callback, plus N virtual function calls. This can save us a large amount of performance overhead because even though we're calling virtual functions (which cost a small overhead more than non-virtual function calls because they need to redirect the call to the correct place), we're still keeping the overwhelming majority of update behavior inside our managed code and avoiding the Native-Managed Bridge as much as possible. This class can even be expanded to provide priority systems, to skip low-priority tasks if it detects that the current frame has taken too long, and many other possibilities.

Depending on how deep you already are into your current project, such changes can be incredibly daunting, time-consuming, and likely to introduce a lot of bugs as subsystems are updated to make use of a completely different set of dependencies. However, the benefits can outweigh the risks if time is on your side. It would be wise to do some testing on a group of objects in a scene that is similarly designed to your current scene files to verify that the benefits outweigh the costs.