Update, Coroutines, and InvokeRepeating

Update is called every frame, but sometimes we hack in ways for the Update to be called less frequently than normal, and perhaps, without realizing it, we create a situation where an empty method is called more often than not:

void Update() {
  _timer += Time.deltaTime;
  if (_timer > _aiUpdateFrequency) {
    ProcessAI();
    _timer -= _aiUpdateFrequency;
  }
}

With this function definition, we are essentially calling an empty function almost every frame. In fact, it is worse than that; we're also performing a Boolean check that almost always returns false. This is fine if we don't abuse the concept, but as you've learned, having too many of these unnecessary function calls hiding in our Scene can be a sneaky hit on our performance.

This function is a perfect example of a function we can convert into a Coroutine to make use of their delayed-invocation properties:

void Start() {
  StartCoroutine(UpdateAI());
}

IEnumerator UpdateAI() {
  while (true) {
    yield return new WaitForSeconds(_aiUpdateFrequency);
    ProcessAI();
  }
}

However, this approach has its drawbacks. For one, a Coroutine comes with an additional overhead cost relative to a standard function call (around twice as slow), as well as some heap memory allocations to store the current state in memory until the next time it is invoked. Secondly, once initialized, Coroutines run independent, of the GameObject's Update() process and will be invoked regardless of whether the GameObject has been disabled or not. Care must be taken before deciding to adopt this approach.

However, if the situation is appropriate, the benefits of calling nothing during most frames, often outweighs the additional cost during the frames where it is invoked. Indeed, if we aren't performing too many complex things with yield statements, then we can often create an even simpler version of the method using InvokeRepeating(), which has a slightly smaller overhead cost (about 1.5 times slower than a standard function call):

void Start() {
  InvokeRepeating("ProcessAI", 0f, _aiUpdateFrequency);
}

Note that InvokeRepeating is also independent of the GameObject's Update() method, and will continue to be invoked even if the object is disabled.

Regardless of which approach we pick, there is an additional risk—having too many methods triggering in the same frame simultaneously. Imagine thousands of these objects that initialized together during Scene initialization. Every time _aiUpdateFrequency seconds go by, they will all invoke the ProcessAI() method within the same frame, and cause a huge spike in CPU usage.

Possible solutions to this problem include:

  • Generating a new random time to wait each time the timer expires or Coroutine triggers
  • Spread out Coroutine initialization so that only a handful of them are started each frame
  • Delegate the responsibility of calling updates to some master class that places a limit on the number of invocations that occur each frame

Reducing excessive Update definitions to a simple Coroutine can potentially save us a lot of unnecessary overhead, so we should consider converting them whenever we feel it is appropriate. However, we might also consider re-evaluating our solution to the original problem in order to prevent lots of timed events all triggering at the same moment.

Another approach to optimizing updates is to not use Update() at all, or more accurately, to only use it once. When Unity calls Update(), it involves bridging the gap between a GameObject's native and managed representation of the GameObject, which can be a costly task. You will learn more about this native-managed bridge in Chapter 7, Masterful Memory Management, but for now, just consider that every callback we rely on Unity to invoke for us comes with a hidden processing cost attached relative to a standard function call.

We can therefore minimize this overhead by limiting how often it needs to cross the bridge. We can do this by having a god class take care of calling our own custom update-style method across all of our custom Components, in its own definition of Update(). In fact, many Unity developers prefer this approach right from the start of their projects, as it gives them finer control over when and how updates propagate throughout the system, for things such as menu pausing and cool time manipulation effects.

All objects wanting to integrate with such a system must have a common entry point. We can achieve this through an interface class. Interfaces essentially set up a contract whereby any class that implements the interface must provide a specific series of methods. In other words, if we know the object implements an interface, 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 interfaces (this avoids the "deadly diamond of death" problem that C++ programmers may be familiar with).

The following interface definition will suffice, which only requires the implementing class to define a single method:

public interface IUpdateable {
  void OnUpdate(float dt);
}

Next, we'll define a MonoBehaviour class, which implements this interface:

public class UpdateableMonoBehaviour : 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 standard Update() callback.

The OnUpdate() method of the UpdateableMonoBehaviour class retrieves the current delta time (dt), to spare us from a bunch of unnecessary Time.deltaTime calls. We've also made the function virtual, to allow derived classes to customize it. However, as you know, Unity automatically grabs and invokes methods defined with the name Update(), but since we're defining our own custom update with a different name, then we need to implement something that will call this method when the time is appropriate; some kind of "GameLogic" god class.

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 a Singleton Component, as defined earlier in the section entitled Singleton Components, and has appropriate static functions defined for registration and deregistration (although bear in mind that it can just as easily use our messaging system!).

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

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

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

It is best to use the Start() method for this task, 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, then 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.

For example:

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

protected virtual void Initialize() {
  // derived classes should override this method for initialization code
}

We should try to make the process as automatic as possible to spare ourselves having to re-implement these tasks for each new Component we define. As soon as a class inherits from our UpdateableMonoBehaviour class, then should be secure in the knowledge that its OnUpdate() method will be called whenever it is appropriate.

Finally, we need to implement the GameLogic class. The implementation is pretty much the same whether it is a Singleton Component or a standalone Component, and whether or not it uses the MessagingSystem. Either way, our UpdateableMonoBehaviour class must register and deregister as IUpdateableObject 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 class definition for the GameLogic system:

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

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

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

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

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

}

If we make sure all of our custom MonoBehaviours inherit from the UpdateableMonoBehaviour 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, we're still keeping the overwhelming majority of update behavior inside managed code, and avoiding the native-managed bridge as much as possible.

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 if the benefits outweigh the costs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.9.133