How to create an early and a super late update in Unity

Posted by : on

Category : Unity

The Unity game loop

You can check the final proof of concept project on github

The Unity Engine provides us with event functions on MonoBehaviours that get executed automatically, some of those methods are part of the game loop. The game loop is the core part of Unity that is responsible for repeating endlessly so that our game runs, some parts of this game loop system are easily accessible to us, like the Update and FixedUpdate methods, other parts are not accessible to us, because their responsibilities are fixed in the Engine.

Unity gives us some limited access to that game loop system via the UnityEngine.LowLevel namespace, by providing three static methods and the PlayerLoopSystem struct with its properties that can help us add/remove or change/replace some of the systems in the game loop. Although removing some systems may have negative consequences to our game, like the physics not being calculated and changing some systems gets tricky, because we will have to deal with unsafe code that is marshalling into the C++ space, adding our own systems into the mix is relatively harmless and pretty easy.

The following post shows a way to add an EarlyUpdate, that gets executed before any Update method has run and a SuperLateUpdate that gets executed after all LateUpdates have executed. You can use it as a guideline to add your own systems wherever you feel necessary, or to remove any systems that you may feel that you don’t need, although i wouldn’t recommend that because it would be easy to inadvertently break something.

Changing a system by adding some functionality at the beginning or the end is also possible, but it gets trickier as we will need to use unsafe code and the compatibility of it, is dependent on the hardware and the compilation method we will use and for that reason it is out of the scope of this post.

The player loop system

The PlayerLoop class provides us with three static methods: GetCurrentPlayerLoop, GetDefaultPlayerLoop and SetPlayerLoop. The first two return the current and default update order of all engine systems in Unity and the third sets a new custom order.

Let’s start by taking a look at the default update order of Unity’s engine systems:

public static class DefaultUpdateOrder
{
   [RuntimeInitializeOnLoadMethod]
   private static void Init()
   {
      StringBuilder sb = new();
      ShowPlayerLoop(PlayerLoop.GetDefaultPlayerLoop(), sb, 0);
      Debug.Log(sb);
   }

   private static void ShowPlayerLoop(PlayerLoopSystem playerLoopSystem, StringBuilder text, int inline)
   {
      if (playerLoopSystem.type != null)
      {
         for (var i = 0; i < inline; i++)
         {
            text.Append("\t");
         }
         text.AppendLine(playerLoopSystem.type.Name);
      }

      if (playerLoopSystem.subSystemList != null)
      {
         inline++;
         foreach (var s in playerLoopSystem.subSystemList)
         {
            ShowPlayerLoop(s, text, inline);
         }
      }
   }
}

by running this code the output will be:

TimeUpdate
    WaitForLastPresentationAndUpdateTime
Initialization
    ProfilerStartFrame
    UpdateCameraMotionVectors
    DirectorSampleTime
    AsyncUploadTimeSlicedUpdate
    SynchronizeInputs
    SynchronizeState
    XREarlyUpdate
EarlyUpdate
  PollPlayerConnection
    GpuTimestamp
    AnalyticsCoreStatsUpdate
    UnityWebRequestUpdate
    ExecuteMainThreadJobs
    ProcessMouseInWindow
    ClearIntermediateRenderers
    ClearLines
    PresentBeforeUpdate
    ResetFrameStatsAfterPresent
    UpdateAsyncReadbackManager
    UpdateStreamingManager
    UpdateTextureStreamingManager
    UpdatePreloading
    UpdateContentLoading
    RendererNotifyInvisible
    PlayerCleanupCachedData
    UpdateMainGameViewRect
    UpdateCanvasRectTransform
    XRUpdate
    UpdateInputManager
    ProcessRemoteInput
    ScriptRunDelayedStartupFrame
    UpdateKinect
    DeliverIosPlatformEvents
    ARCoreUpdate
    DispatchEventQueueEvents
    Physics2DEarlyUpdate
    PhysicsResetInterpolatedTransformPosition
    SpriteAtlasManagerUpdate
    PerformanceAnalyticsUpdate
FixedUpdate
    ClearLines
    NewInputFixedUpdate
    DirectorFixedSampleTime
    AudioFixedUpdate
    ScriptRunBehaviourFixedUpdate
    DirectorFixedUpdate
    LegacyFixedAnimationUpdate
    XRFixedUpdate
    PhysicsFixedUpdate
    Physics2DFixedUpdate
    PhysicsClothFixedUpdate
    DirectorFixedUpdatePostPhysics
    ScriptRunDelayedFixedFrameRate
PreUpdate
    PhysicsUpdate
    Physics2DUpdate
    PhysicsClothUpdate
    CheckTexFieldInput
    IMGUISendQueuedEvents
    NewInputUpdate
    SendMouseEvents
    AIUpdate
    WindUpdate
    UpdateVideo
Update
    ScriptRunBehaviourUpdate
    ScriptRunDelayedDynamicFrameRate
    ScriptRunDelayedTasks
    DirectorUpdate
PreLateUpdate
    AIUpdatePostScript
    DirectorUpdateAnimationBegin
    LegacyAnimationUpdate
    DirectorUpdateAnimationEnd
    DirectorDeferredEvaluate
    UIElementsUpdatePanels
    EndGraphicsJobsAfterScriptUpdate
    ConstraintManagerUpdate
    ParticleSystemBeginUpdateAll
    Physics2DLateUpdate
    PhysicsLateUpdate
    ScriptRunBehaviourLateUpdate
PostLateUpdate
    PlayerSendFrameStarted
    DirectorLateUpdate
    ScriptRunDelayedDynamicFrameRate
    PhysicsSkinnedClothBeginUpdate
    UpdateRectTransform
    PlayerUpdateCanvases
    UpdateAudio
    VFXUpdate
    ParticleSystemEndUpdateAll
    EndGraphicsJobsAfterScriptLateUpdate
    UpdateCustomRenderTextures
    XRPostLateUpdate
    UpdateAllRenderers
    UpdateLightProbeProxyVolumes
    EnlightenRuntimeUpdate
    UpdateAllSkinnedMeshes
    ProcessWebSendMessages
    SortingGroupsUpdate
    UpdateVideoTextures
    UpdateVideo
    DirectorRenderImage
    PlayerEmitCanvasGeometry
    PhysicsSkinnedClothFinishUpdate
    FinishFrameRendering
    BatchModeUpdate
    PlayerSendFrameComplete
    UpdateCaptureScreenshot
    PresentAfterDraw
    ClearImmediateRenderers
    PlayerSendFramePostPresent
    UpdateResolution
    InputEndFrame
    TriggerEndOfFrameCallbacks
    GUIClearEvents
    ShaderHandleErrors
    ResetInputAxis
    ThreadedLoadingDebug
    ProfilerSynchronizeStats
    MemoryFrameMaintenance
    ExecuteGameCenterCallbacks
    XRPreEndFrame
    ProfilerEndFrame
    GraphicsWarmupPreloadedShaders

WOW!, that’s a lot. Some of those systems though are known to people that have used Unity even for 5 minutes. What is important to notice here is that each system is composed by subsystems that are responsible for certain parts of the core game loop, that’s why it is important not to remove/replace something unless we are pretty sure we know what we are doing.

Adding something though is relatively harmless as i mentioned and can be pretty useful depending on our situation.

The PlayerLoopSystem struct properties

The PlayerLoopSystem struct, exposes 5 properties that we can use to achieve our goals:

  • loopConditionFunction: The loop condition for a native engine system
  • subSystemList: We used this property to get the subsystems of each system in our previous code. Each PlayerLoopSystem struct contains a list of PlayerLoopSystem subsystems which in turn can contain their own etc.
  • type: A type that acts as an ID for a subsystem and shows where this subsystem belongs, for custom systems this is only useful as an ID for the profiler.
  • updateDelegate: A delegate that acts as an entry point to the game loop, useful for calling our own method when is the time for our own system to run.
  • updateFunction: A native engine system, the only valid values are values that get returned by a PlayerLoopSystem that exists in the default game loop. This returns a pointer to the C++ native methods.

First let’s create our first system, our EarlyUpdate:

var myEarlyUpdate = new PlayerLoopSystem
{
    subSystemList = null,
    updateDelegate = OnEarlyUpdate,
    type = typeof(MyEarlyUpdate)
};

the MyEarlyUpdate is an empty class that its only purpose is to act as an ID:

public class MyEarlyUpdate { }

the OnEarlyUpdate will be the method that is called when it is time for our system to run.

Adding our own system to the loop

Now we need to create a method that adds our system to the loop, we will do that by creating a new PlayerLoopSystem and adding to that each subsystem of an already existing loop. We will be searching for a type of subsystem after which we will add our own and then keep adding the remaining subsystems.

private static PlayerLoopSystem AddSystem<T>(in PlayerLoopSystem loopSystem, PlayerLoopSystem systemToAdd) where T : struct
{
    PlayerLoopSystem newPlayerLoop = new()
    {
        loopConditionFunction = loopSystem.loopConditionFunction,
        type = loopSystem.type,
        updateDelegate = loopSystem.updateDelegate,
        updateFunction = loopSystem.updateFunction
    };

   List<PlayerLoopSystem> newSubSystemList = new();

   foreach (var subSystem in loopSystem.subSystemList)
   {
        newSubSystemList.Add(subSystem);
      
        if (subSystem.type == typeof(T))
            newSubSystemList.Add(systemToAdd);
   }

   newPlayerLoop.subSystemList = newSubSystemList.ToArray();
   return newPlayerLoop;
}

the generic T here is the system that runs exactly before our own, the loopSystem parameter is the loopSystem we are iterating and the systemToAdd parameter is the system we have created. Obviously the code can be modified so that we can add our system before the T system. In the end we return the new system and we can set it with the statement:

PlayerLoop.SetPlayerLoop(ourSystem);

after that, everything that is in code inside our methods that we have provided in the Delegate will run at the appropriate time.

Here is the full code for the Early and LateUpdate systems:

public class MySuperLateUpdate { } // Empty class that acts as an identifier for our SuperLateUpdate system
public class MyEarlyUpdate { } // Empty class that acts as an identifier for our EarlyUpdate system

public static class MySystem
{
   public static event Action AddSuperLateUpdate;
   public static event Action AddEarlyUpdate;

   [RuntimeInitializeOnLoadMethod]
   private static void Init()
   {
      var defaultSystems = PlayerLoop.GetDefaultPlayerLoop();

      var mySuperLateUpdate = new PlayerLoopSystem
      {
         subSystemList = null,
         updateDelegate = OnSuperLateUpdate,
         type = typeof(MySuperLateUpdate)
      };
      
      var myEarlyUpdate = new PlayerLoopSystem
      {
         subSystemList = null,
         updateDelegate = OnEarlyUpdate,
         type = typeof(MyEarlyUpdate)
      };

      var loopWithSuperLateUpdate = AddSystem<PreLateUpdate>(in defaultSystems, mySuperLateUpdate);
      var loopWithEarlyAndSuperLateUpdate = AddSystem<PreUpdate>(in loopWithSuperLateUpdate, myEarlyUpdate);
      PlayerLoop.SetPlayerLoop(loopWithEarlyAndSuperLateUpdate);
    }

   private static PlayerLoopSystem AddSystem<T>(in PlayerLoopSystem loopSystem, PlayerLoopSystem systemToAdd) where T : struct
   {
      PlayerLoopSystem newPlayerLoop = new()
      {
         loopConditionFunction = loopSystem.loopConditionFunction,
         type = loopSystem.type,
         updateDelegate = loopSystem.updateDelegate,
         updateFunction = loopSystem.updateFunction
      };

      List<PlayerLoopSystem> newSubSystemList = new();

      foreach (var subSystem in loopSystem.subSystemList)
      {
         newSubSystemList.Add(subSystem);
         
         if (subSystem.type == typeof(T))
            newSubSystemList.Add(systemToAdd);
      }

      newPlayerLoop.subSystemList = newSubSystemList.ToArray();
      return newPlayerLoop;
   }

   private static void OnEarlyUpdate() => AddEarlyUpdate?.Invoke();

   private static void OnSuperLateUpdate() => AddSuperLateUpdate?.Invoke();
}

By subscribing to the AddEarlyUpdate and AddSuperLateUpdate from any script, we can have functionality that runs on those systems. Like this:

public class Test : MonoBehaviour
{
    private void OnEnable()
    {
        MySystem.AddEarlyUpdate += MyEarlyUpdate;
        MySystem.AddSuperLateUpdate += MySuperLateUpdate;
    }

    private void Update()
    {
        Debug.Log("In Update");
    }

    private void LateUpdate()
    {
        Debug.Log("In late Update");
    }

    private void OnDisable()
    {
        MySystem.AddEarlyUpdate -= MyEarlyUpdate;
        MySystem.AddSuperLateUpdate -= MySuperLateUpdate;
    }

    private void MySuperLateUpdate()
    {
        Debug.Log("Super Late Update Running!");
    }

    private void MyEarlyUpdate()
    {
        Debug.Log("Early Update Running!");
    }
}

if we try now to see our player loop by calling

ShowPlayerLoop(PlayerLoop.GetCurrentPlayerLoop(), sb2, 0);

we can see that our new systems are in place:

TimeUpdate
    WaitForLastPresentationAndUpdateTime
Initialization
    ProfilerStartFrame
    UpdateCameraMotionVectors
    DirectorSampleTime
    AsyncUploadTimeSlicedUpdate
    SynchronizeInputs
    SynchronizeState
    XREarlyUpdate
EarlyUpdate
    PollPlayerConnection
    GpuTimestamp
    AnalyticsCoreStatsUpdate
    UnityWebRequestUpdate
    ExecuteMainThreadJobs
    ProcessMouseInWindow
    ClearIntermediateRenderers
    ClearLines
    PresentBeforeUpdate
    ResetFrameStatsAfterPresent
    UpdateAsyncReadbackManager
    UpdateStreamingManager
    UpdateTextureStreamingManager
    UpdatePreloading
    UpdateContentLoading
    RendererNotifyInvisible
    PlayerCleanupCachedData
    UpdateMainGameViewRect
    UpdateCanvasRectTransform
    XRUpdate
    UpdateInputManager
    ProcessRemoteInput
    ScriptRunDelayedStartupFrame
    UpdateKinect
    DeliverIosPlatformEvents
    ARCoreUpdate
    DispatchEventQueueEvents
    Physics2DEarlyUpdate
    PhysicsResetInterpolatedTransformPosition
    SpriteAtlasManagerUpdate
    PerformanceAnalyticsUpdate
FixedUpdate
    ClearLines
    NewInputFixedUpdate
    DirectorFixedSampleTime
    AudioFixedUpdate
    ScriptRunBehaviourFixedUpdate
    DirectorFixedUpdate
    LegacyFixedAnimationUpdate
    XRFixedUpdate
    PhysicsFixedUpdate
    Physics2DFixedUpdate
    PhysicsClothFixedUpdate
    DirectorFixedUpdatePostPhysics
    ScriptRunDelayedFixedFrameRate
PreUpdate
    PhysicsUpdate
    Physics2DUpdate
    PhysicsClothUpdate
    CheckTexFieldInput
    IMGUISendQueuedEvents
    NewInputUpdate
    SendMouseEvents
    AIUpdate
    WindUpdate
    UpdateVideo
MyEarlyUpdate
Update
    ScriptRunBehaviourUpdate
    ScriptRunDelayedDynamicFrameRate
    ScriptRunDelayedTasks
    DirectorUpdate
PreLateUpdate
    AIUpdatePostScript
    DirectorUpdateAnimationBegin
    LegacyAnimationUpdate
    DirectorUpdateAnimationEnd
    DirectorDeferredEvaluate
    UIElementsUpdatePanels
    EndGraphicsJobsAfterScriptUpdate
    ConstraintManagerUpdate
    ParticleSystemBeginUpdateAll
    Physics2DLateUpdate
    PhysicsLateUpdate
    ScriptRunBehaviourLateUpdate
MySuperLateUpdate
PostLateUpdate
    PlayerSendFrameStarted
    DirectorLateUpdate
    ScriptRunDelayedDynamicFrameRate
    PhysicsSkinnedClothBeginUpdate
    UpdateRectTransform
    PlayerUpdateCanvases
    UpdateAudio
    VFXUpdate
    ParticleSystemEndUpdateAll
    EndGraphicsJobsAfterScriptLateUpdate
    UpdateCustomRenderTextures
    XRPostLateUpdate
    UpdateAllRenderers
    UpdateLightProbeProxyVolumes
    EnlightenRuntimeUpdate
    UpdateAllSkinnedMeshes
    ProcessWebSendMessages
    SortingGroupsUpdate
    UpdateVideoTextures
    UpdateVideo
    DirectorRenderImage
    PlayerEmitCanvasGeometry
    PhysicsSkinnedClothFinishUpdate
    FinishFrameRendering
    BatchModeUpdate
    PlayerSendFrameComplete
    UpdateCaptureScreenshot
    PresentAfterDraw
    ClearImmediateRenderers
    PlayerSendFramePostPresent
    UpdateResolution
    InputEndFrame
    TriggerEndOfFrameCallbacks
    GUIClearEvents
    ShaderHandleErrors
    ResetInputAxis
    ThreadedLoadingDebug
    ProfilerSynchronizeStats
    MemoryFrameMaintenance
    ExecuteGameCenterCallbacks
    XRPreEndFrame
    ProfilerEndFrame
    GraphicsWarmupPreloadedShaders

Making it cleaner

Using events is not very performant, a better approach would be to create a publisher/subscriber pattern through interfaces. Every time that we want one of our scripts to have the EarlyUpdate or the SuperLateUpdate we will make it implement the corresponding interface and then subscribe to our static manger class, like this:

public class Square : MonoBehaviour, IEarlyUpdate
{
    private void OnEnable()
    {
        UpdateManager.RegisterEarlyUpdate(this);
    }

    private void OnDisable()
    {
        UpdateManager.UnregisterEarlyUpdate(this);
    }

    private void Update()
    {
        Debug.Log("Inside square Update");
    }

    void IEarlyUpdate.EarlyUpdate()
    {
        Debug.Log("Inside Square Early Update");
    }
}

Our static UpdateManager class then would look like this:

public static class UpdateManager
{
   private static readonly HashSet<IEarlyUpdate> _earlyUpdates = new();
   private static readonly HashSet<ISuperLateUpdate> _superLateUpdates = new();

   public static void RegisterEarlyUpdate(IEarlyUpdate earlyUpdate) => _earlyUpdates.Add(earlyUpdate);

   public static void RegisterSuperLateUpdate(ISuperLateUpdate superLateUpdate) => _superLateUpdates.Add(superLateUpdate);

   public static void UnregisterEarlyUpdate(IEarlyUpdate earlyUpdate) => _earlyUpdates.Remove(earlyUpdate);

   public static void UnregisterSuperLateUpdate(ISuperLateUpdate superLateUpdate) => _superLateUpdates.Remove(superLateUpdate);

   [RuntimeInitializeOnLoadMethod]
   private static void Init()
   {
      var defaultSystems = PlayerLoop.GetDefaultPlayerLoop();

      var mySuperLateUpdate = new PlayerLoopSystem
      {
         subSystemList = null,
         updateDelegate = OnSuperLateUpdate,
         type = typeof(MySuperLateUpdate)
      };
      
      var myEarlyUpdate = new PlayerLoopSystem
      {
         subSystemList = null,
         updateDelegate = OnEarlyUpdate,
         type = typeof(MyEarlyUpdate)
      };

      var loopWithSuperLateUpdate = AddSystem<PreLateUpdate>(in defaultSystems, mySuperLateUpdate);
      var loopWithEarlyAndSuperLateUpdate = AddSystem<PreUpdate>(in loopWithSuperLateUpdate, myEarlyUpdate);
      PlayerLoop.SetPlayerLoop(loopWithEarlyAndSuperLateUpdate);
      
      static PlayerLoopSystem AddSystem<T>(in PlayerLoopSystem loopSystem, PlayerLoopSystem systemToAdd) where T : struct
      {
         PlayerLoopSystem newPlayerLoop = new()
         {
            loopConditionFunction = loopSystem.loopConditionFunction,
            type = loopSystem.type,
            updateDelegate = loopSystem.updateDelegate,
            updateFunction = loopSystem.updateFunction
         };

         List<PlayerLoopSystem> newSubSystemList = new();

         foreach (var subSystem in loopSystem.subSystemList)
         {
            newSubSystemList.Add(subSystem);
         
            if (subSystem.type == typeof(T))
               newSubSystemList.Add(systemToAdd);
         }

         newPlayerLoop.subSystemList = newSubSystemList.ToArray();
         return newPlayerLoop;
      }
   }
   
   private static void OnEarlyUpdate()
   {
      using var e = _earlyUpdates.GetEnumerator();
      while (e.MoveNext())
      {
         e.Current?.EarlyUpdate();
      }
   }

   private static void OnSuperLateUpdate()
   {
      using var e = _superLateUpdates.GetEnumerator();
      while (e.MoveNext())
      {
         e.Current?.SuperLateUpdate();
      }
   }
}

for a full proof of concept, you can check the code on github

Conclusion

Having our own systems in the player loop is not only useful but can actually be more performant. Unity has to move between C# and C++ space for every event method that we use in our scripts. By using our own system, for example the EarlyUpdate instead of the Unity’s Update event method, is like having a manager class that has an Update that is calling MyUpdate methods from our scripts instead of having each script implement its own Update method.

Besides that, now, we are not constrained by MonoBehaviours. A normal C# class can implement the proper interface and after registering a method, can have that method executed every time inside the game loop. Again the project on github has the NonMonobehaviour class that is subscribed to the new systems.

Thank you for reading and as always if you have any questions or comments you can use the comments section or contact me directly via the contact form or email, also if you don’t want to miss any of the new articles, you can always subscribe to my newsletter or the RSS feed.


Follow me: