Wednesday, April 29, 2020

One Single Update () function

public class SingletonAsComponent<T> : MonoBehaviour where T : 
SingletonAsComponent<T> {
  
  private static T __Instance;
  
  protected static SingletonAsComponent<T> _Instance {
      get {
        if(!__Instance) {
              T [] managers = 
              GameObject.FindObjectsOfType(typeof(T)) as T[];
              if (managers != null) {
                  if(managers.Length == 1) {
                      __Instance = managers[0];
                      return __Instance;
                  } else if (managers.Length > 1) {
                      Debug.LogError("You have more than one " + 
                      typeof(T).Name + " in the scene. You only 
                      need 1, it's a singleton!");
                      for(int i = 0; i < managers.Length; ++i) {
                          T manager = managers[i];
                          Destroy(manager.gameObject);
                      }
                  }
              }

              GameObject go = new GameObject(typeof(T).Name, 
              typeof(T));
              __Instance = go.GetComponent<T>();
              DontDestroyOnLoad(__Instance.gameObject);
          }
          return __Instance;
      } 
      set {
          __Instance = value as T;
      }
  }
}
public class MySingletonComponent : SingletonAsComponent<MySingletonComponent> {
  public static MySingletonComponent Instance {
    get { return ((MySingletonComponent)_Instance); }
    set { _Instance = value; }
  }
}
This class can be used at runtime by having any other object access the Instance property at any time. If the Component does not already exist in our Scene, then the SingletonAsComponent base class will instantiate its own GameObject and attach an instance of the derived class to it as a Component. From that point forward, access through the Instance property will reference the Component that was created.
Proper cleanup of a Singleton Component can be a little convoluted because of how Unity tears down Scenes. An object's OnDestroy() method is called whenever it is destroyed during runtime. The same method is called during application shutdown, whereby every Component on every GameObject has its OnDestroy() method called by Unity. Application shutdown also takes place when we end Play Mode in the Editor and return to Edit Mode. However, destruction of objects occurs in a random order, and we cannot assume that the Singleton Component will be the last object destroyed.
Consequently, if any object attempts to do anything with the Singleton in the middle of their OnDestroy() method, then they will be calling the Instance property. If the Singleton has already been destroyed prior to this moment, then calling Instance during another object's destruction would create a new instance of the Singleton Component in the middle of application shutdown! This can corrupt our Scene files, as instances of our Singleton Components will be left behind in the Scene. If this happens, then Unity will throw the following error message at us:
The reason some objects may wish to call into our Singleton during destruction is that Singletons often make use of the Observer pattern. This design pattern allows other objects to register/deregister with them for certain tasks, similar to how Unity latches onto callback methods, but in a less automated fashion. We will see an example of this in the upcoming section A global messaging system. Objects that are registered with the system during construction will want to deregister with the system during their own shutdown, and the most convenient place to do this is within its OnDestroy() method. Consequently, such objects are likely to run into the aforementioned problem, where Singletons are accidentally created during application shutdown.


To solve this problem, we need to make three changes. Firstly, we need to add an additional flag to the Singleton Component, which keeps track of its active state, and disable it at the appropriate times. This includes the Singleton's own destruction, as well as application shutdown (OnApplicationQuit() is another useful Unity callback for MonoBehaviours, which is called during this time):
The following interface definition will suffice, which only requires the implementing class to define a single method:
private bool _alive = true;
void OnDestroy() { _alive = false; }
void OnApplicationQuit() { _alive = false; }
Secondly, we should implement a way for external objects to verify the Singleton's current state:
public static bool IsAlive {
  get {
    if (__Instance == null)
      return false;
    return __Instance._alive;
  }
}
Finally, any object that attempts to call into the Singleton during its own OnDestroy() method, must first verify the state using the IsAlive property before calling Instance. For example:
public class SomeComponent : MonoBehaviour {
    void OnDestroy() {
    if (MySingletonComponent.IsAlive) {
        MySingletonComponent.Instance.SomeMethod();
    }
  }
}
This will ensure that nobody attempts to access Instance during destruction. If we don't follow this rule, then we will run into problems where instances of our Singleton object will be left behind in the Scene after returning to Edit Mode.
The irony of the Singleton Component approach is that we are using one of Unity's Find() methods to determine whether or not one of these Singleton Components already exists in the Scene before we attempt to assign the __Instance reference variable. Fortunately, this will only happen when the Singleton Component is first accessed, but it's possible that the initialization of the Singleton would not necessarily occur during Scene initialization and can therefore cost us a performance spike at a bad moment during gameplay, when this object is first instantiated and Find() gets called. The workaround for this is to have some god class confirm that the important Singletons are instantiated during Scene initialization by simply calling Instance on each one.
The downside to this approach is that if we later decide that we want more than one of these manager classes executing at once, or we wish to separate its behavior to be more modular, then there would be a lot of code that needs to change.
There are further alternatives that we can explore, such as making use of Unity's built-in bridge between script code and the Inspector interface.
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.
Usage:
Many MonoBehaviours inherit from UpdateableMonoBehaviour 

UpdateableMonoBehaviour implement
public class UpdateableMonoBehaviour : MonoBehaviour, IUpdateable 
{
  public virtual void OnUpdate(float dt) {}
  void Start() {
  GameLogic.Instance.RegisterUpdateableObject(this);
  Initialize();
  }

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

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

No comments:

Post a Comment