Search Unity

Efficient Updating - Take Control of Update Functions

Discussion in 'Community Learning & Teaching' started by RobAnthem, Jun 25, 2017.

  1. RobAnthem

    RobAnthem

    Joined:
    Dec 3, 2016
    Posts:
    90
    Efficient Updating System
    In this tutorial we will cover how to bypass the Unity Update() functions to be less resource-intensive and perform faster. Studies show that handling Updates yourself is 4X or more faster than letting Unity handle them.

    The basic idea behind this is that we only use Update when absolutely needed, and move all our updating functions to a single Object that receives the standard Update() callbacks.

    The basic hierarchy of the alternative Update System looks like this.

    MonoBehaviour
    Updater Object
    Update Queue
    Updated Objects
    Removal Queue
    Add Queue
    The first step to creating a custom Update system is to create an Interface for our objects that need Updated. If you aren't familiar with Interfaces, they are essentially templates for data and functions that any other class can inherit from, however they can't be instanced without inheritance, but we can still use calls to the Interface as though it was instanced as its own class. As such, all objects that require an Update will inherit from our Interface.

    So let's go into Visual Studios or whatever IDE you use, and create a new C# class called IUpdate and instead of calling it a class, we will call it an interface, so add the following lines to the file.

    Code (CSharp):
    1. namespace MyProjectName
    2. {
    3.     public interface IUpdate
    4.     {
    5.         bool NeedsUpdate { get; set; }
    6.         void PerformUpdate(float time);
    7.         bool NeedsFixedUpdate { get; set; }
    8.         void PerformFixedUpdate(float time);
    9.         bool NeedsFinalUpdate { get; set; }
    10.         void PerformFinalUpdate(float time);
    11.         bool NeedsLateUpdate { get; set; }
    12.         void PerformLateUpdate(float time);
    13.     }
    14. }
    As you can see, we added a bool for each update type, this will need to be defined after you inherit from the interface. The purpose of the bool is so that when the Updater initializes, it can determine what sort of Update(s) will be required by the IUpdate object and add them to the appropriate Update queue.

    At the moment, obviously, this interface doesn't do anything, so let's create our Updater MonoBehaviour object that will handle various Updates. Now remember, this Update system will also support Non-MonoBehaviours being registered and Updated. So if you have a data-class that is time-sensitive, then you can use the IUpdate interface to get Update callbacks with a time amount.

    Our second class is going to handle Updating all types of Update, as well as registering and unregistering IUpdate objects. So that anything that is instantiated or disabled during run-time, can have it's appropriate Update function called or removed.

    So create a new class called Updater, and inherit from MonoBehaviour so we can receive the default Update callbacks.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. namespace MyProjectName
    4. {
    5.     public class Updater : MonoBehaviour
    6.     {
    7.  
    8.     }
    9. }
    10.  
    Now due to the fact that we will need constant access to this class, we should make it Singleton. If you aren't familiar with Singleton, it basically means that we only ever plan to have a single instance of this class at any given time. So to do that, we create a static member of itself inside the class, that then gets assigned on Awake/Creation depending on the type of class. For MonoBehaviour Singleton, we will use the Awake() function to ensure its existence, but also create a redundancy in-case another object attempts to access the singleton instance before it is initialized. So add these Singleton Instance fields to the class, like this.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. namespace Cubening
    4. {
    5.     public class Updater : MonoBehaviour
    6.     {
    7.         public static Updater Instance
    8.         {
    9.             get
    10.             {
    11.                 if (instance == null)
    12.                 {
    13.                     instance = FindObjectOfType<Updater>();
    14.                 }
    15.                 return instance;
    16.             }
    17.             set
    18.             {
    19.                 instance = value;
    20.             }
    21.         }
    22.         private static Updater instance;
    23.     }
    24. }
    25.  
    This is a pretty simple idea, and it can be extended in many ways, for now though, we only need our Singleton Instance to work like this.

    Now we need to add our various List Queues for Updating purposes. For each Update type we will need an Update Queue, an Add Queue, and a Removal Queue. The Add and Remove Queue will act as a preventive measure to ensure our Queue doesn't change during iteration, only before it.

    So add the following lines to the Updater class.

    Code (CSharp):
    1.         private List<IUpdate> UpdateQueue = new List<IUpdate>();
    2.         private List<IUpdate> FixedUpdateQueue = new List<IUpdate>();
    3.         private List<IUpdate> FinalUpdateQueue = new List<IUpdate>();
    4.         private List<IUpdate> LateUpdateQueue = new List<IUpdate>();
    5.         private List<IUpdate> UpdateAddQueue = new List<IUpdate>();
    6.         private List<IUpdate> FixedUpdateAddQueue = new List<IUpdate>();
    7.         private List<IUpdate> FinalUpdateAddQueue = new List<IUpdate>();
    8.         private List<IUpdate> LateUpdateAddQueue = new List<IUpdate>();
    9.         private List<IUpdate> UpdateRemovalQueue = new List<IUpdate>();
    10.         private List<IUpdate> FixedUpdateRemovalQueue = new List<IUpdate>();
    11.         private List<IUpdate> FinalUpdateRemovalQueue = new List<IUpdate>();
    12.         private List<IUpdate> LateUpdateRemovalQueue = new List<IUpdate>();
    As you can see, we have 12 Lists, 3 for each Update type. The Add and Removal Queues will be called at the top of each Update function to adjust our main Queues before they are interated.

    To utilize our Add and Remove Queues we will need to create some methods for filling the lists. The most efficient way is likely to create an Enum for Update Type and pass the IUpdate object and the enum into the method. So let's create the Enum and base add/remove functions like this.

    Code (CSharp):
    1.         public enum UpdateType { Update, FixedUpdate, LateUpdate, FinalUpdate }
    2.         public void RegisterUpdate(IUpdater script, UpdateType updateType)
    3.         {
    4.             if (updateType == UpdateType.Update)
    5.             {
    6.                 UpdateAddQueue.Add(script);
    7.             }
    8.             else if (updateType == UpdateType.FixedUpdate)
    9.             {
    10.                 FixedUpdateAddQueue.Add(script);
    11.             }
    12.             else if (updateType == UpdateType.LateUpdate)
    13.             {
    14.                 LateUpdateAddQueue.Add(script);
    15.             }
    16.             else
    17.             {
    18.                 FinalUpdateAddQueue.Add(script);
    19.             }
    20.         }
    21.         public void UnegisterUpdate(IUpdater script, UpdateType updateType)
    22.         {
    23.             if (updateType == UpdateType.Update)
    24.             {
    25.                 UpdateRemovalQueue.Add(script);
    26.             }
    27.             else if (updateType == UpdateType.FixedUpdate)
    28.             {
    29.                 FixedUpdateRemovalQueue.Add(script);
    30.             }
    31.             else if (updateType == UpdateType.LateUpdate)
    32.             {
    33.                 LateUpdateRemovalQueue.Add(script);
    34.             }
    35.             else
    36.             {
    37.                 FinalUpdateRemovalQueue.Add(script);
    38.             }
    39.         }
    This will make it so that we can register new objects, or remove old ones from the Queues, which is incredibly important when procedural generation is involved, or just spawning/despawning entities.

    Now we can add our standard Unity Update functions in, and have them iterate through each type of Queue.
    Each function will look like this, with it;s corresponding Update Queues called.

    Code (CSharp):
    1.         void Update()
    2.         {
    3.             if (UpdateAddQueue.Count > 0)
    4.             {
    5.                 for (int i = UpdateAddQueue.Count - 1; i >= 0; i--)
    6.                 {
    7.                     UpdateQueue.Add(UpdateAddQueue[i]);
    8.                     UpdateAddQueue.Remove(UpdateAddQueue[i]);
    9.                 }
    10.             }
    11.             if (UpdateRemovalQueue.Count > 0)
    12.             {
    13.                 for (int i = UpdateRemovalQueue.Count - 1; i >= 0; i--)
    14.                 {
    15.                     UpdateQueue.Remove(UpdateRemovalQueue[i]);
    16.                     UpdateRemovalQueue.Remove(UpdateRemovalQueue[i]);
    17.                 }
    18.             }
    19.             foreach (IUpdate queued in UpdateQueue)
    20.             {
    21.                 queued.PerformUpdate(Time.deltaTime);
    22.             }
    23.         }
    You may of noticed, this is actually a pretty simple function, but it just seems complex because of all the steps involved. The Update function first checks if the Add Queue has any new scripts waiting to be added, and if it does, it adds them to the main Queue and removes them from the Add Queue. Then a similar action is performed for the Removal Queue, after all of that, the Updater can iterate the list of IUpdate objects and call to the custom Update function.

    Now to take it one step further, since often times objects will be pre-existing in the scene, we want our Updater to collect the existing objects and add them to the appropriate Queue. To do that, we will use the FindObjectsOfType<> function like this.

    Code (CSharp):
    1.         void Awake()
    2.         {
    3.             Instance = this;
    4.             MonoBehaviour[] scripts = FindObjectsOfType<MonoBehaviour>();
    5.             foreach (MonoBehaviour script in scripts)
    6.             {
    7.                 if (script is IUpdate)
    8.                 {
    9.                     if ((script as IUpdate).NeedsUpdate)
    10.                     {
    11.                         UpdateQueue.Add((IUpdate)script);
    12.                     }
    13.                     if ((script as IUpdate).NeedsFixedUpdate)
    14.                     {
    15.                         FixedUpdateQueue.Add((IUpdate)script);
    16.                     }
    17.                     if ((script as IUpdate).NeedsLateUpdate)
    18.                     {
    19.                         LateUpdateQueue.Add((IUpdate)script);
    20.                     }
    21.                     if ((script as IUpdate).NeedsFinalUpdate)
    22.                     {
    23.                         FinalUpdateQueue.Add((IUpdate)script);
    24.                     }
    25.                 }
    26.             }
    27.         }
    Now depending on the number of scripts in your scene, this may take a moment to perform, but in most cases it won't even be noticeable. Of course, that's what loading screens are for right?
    As a side note, I've added the Instance declaration into the top of the Awake() function, so that if all goes well, the Singleton can declare its own instance, instead of relying on the FIndObjectOfType function.

    This covers the essentials of the Updater class, after all is said and done, your new Updater should be able to handle any type of required Update, and later we can go over adding a custom System.Timer and your own Update Interval.

    Finally, we will implement our IUpdate interface onto a MonoBehaviour and let the magic happen. Just remember, if the object doesn't exist in the scene already, then it will need to be registered. So let's create a new MonoBehaviour where we would of originally used an Update function.

    Code (CSharp):
    1.     public class UpdateableObject : MonoBehaviour, IUpdater
    2.     {
    3.         void OnEnable()
    4.         {
    5.             /*
    6.              * Enable if this object is not already in the scene during runtime.
    7.              */
    8.             //UpdateHandler.Instance.RegisterUpdate(this, UpdateHandler.UpdateType.Update);
    9.         }
    10.         void OnDisable()
    11.         {
    12.             UpdateHandler.Instance.UnegisterUpdate(this, UpdateHandler.UpdateType.Update);
    13.         }
    14.         public bool NeedsFixedUpdate
    15.         {
    16.             get { return false; }
    17.             set { }
    18.         }
    19.         public bool NeedsUpdate
    20.         {
    21.             get { return true; }
    22.             set { }
    23.         }
    24.         public bool NeedsLateUpdate
    25.         {
    26.             get { return false; }
    27.             set { }
    28.         }
    29.         public bool NeedsFinalUpdate
    30.         {
    31.             get{return false; }
    32.             set{ }
    33.         }
    34.         public void PerformFixedUpdate(float time) { }
    35.         public void PerformUpdate(float time)
    36.         {
    37.             transform.Rotate(transform.right * 20 * time);
    38.         }
    39.         public void PerformLateUpdate(float time) { }
    40.         public void PerformFinalUpdate(float time) { }
    41.     }
    Now because we inherit from IUpdate interface, we no matter what, need to have all the interface properties in our new class, however we can pretty much ignore the ones we aren't using. As evidence by the example, only the Update function is being registered by the Updater, the rest are empty because they won't get called. As well as an example of removing from the Queue on disabling. If you want to test out instantiation during runtime, you can uncomment the OnEnable section, and play with instantiating the object or disabling it.

    This covers the basics of the custom Update handling. I hope this tutorial was helpful and useful.
    If you have any questions about the system or updating, feel free to post questions as replies.

    May the code be with you!
     
    Last edited: Jun 28, 2017
  2. ShamimAkhter

    ShamimAkhter

    Joined:
    Jun 21, 2019
    Posts:
    2
    I found a hidden gem. Awesome Tutorial. I don't understand why there isn't much like or comments in this post.
     
    BuzzyRoboYT likes this.
  3. RobAnthem

    RobAnthem

    Joined:
    Dec 3, 2016
    Posts:
    90
    It's been a while, but I think this is still relevant.
     
    BuzzyRoboYT likes this.
  4. jamespaterson

    jamespaterson

    Joined:
    Jun 19, 2018
    Posts:
    400
    Nicely written! Would be great to see some recent benchmarking vs standard update approach. As i understand it the potential benefit in terms of speed is reducing the number of transitions between managed and unmanaged code, e.g. as outlined here

    https://blogs.unity3d.com/2015/12/23/1k-update-calls/
     
    BuzzyRoboYT likes this.
  5. BuzzyRoboYT

    BuzzyRoboYT

    Joined:
    Jul 6, 2021
    Posts:
    2
    Amazing tutorial!

    I noticed you used IUpdater in 3 places did you mean IUpdate?
     
    Last edited: Dec 23, 2021
  6. Sonistata

    Sonistata

    Joined:
    Nov 22, 2020
    Posts:
    2
    This is a pretty good system, but I recommend having the updater-script not search for all scripts with the interface in awake, because it gives problems when you give other scripts register and unregister functions on enabling and disable

    it doesn't throw a bug it just makes it to where you have two of the same script inside of the UpdateQue because an objects on enable function is called at start

    or another workaround could be to have on the on enable function check to see if the script is already in the update que
     
    o0_Chromium_0o likes this.