Introduction
This is a repost of a blog post I made on my old website, pretty much all of this information still applies in 2020!
Before Unity 5.X (specifically 5.0), control over the application lifecycle was basically nonexistent. For application startup/initialization code you could’ve created a script that was sorted earliest in the Script Execution Order window and used Awake
, but you would’ve ran into issues. An object with that script would need to exist in any scene that required that initialization code to run, a state that is really difficult to maintain while in the editor. It’s too easy to forget that object needs to be around, and that functionality needs to be explained to other developers working on the project.
With Unity 5.0 we were provided RuntimeInitializeOnLoad
, an attribute placed on a static function which is then executed automatically at runtime consistently. Its a foolproof way of ensuring that some code always executes, regardless of scene setup. In 5.2 an optional argument in form of the RuntimeInitializeLoadType
enum was added, allowing developers to decide if the marked function should execute before or after the initial scene is loaded (before the Awake
message is sent). With this single feature it suddenly became viable to use Unity without scenes, slightly closer to using a game framework, such as MonoGame.
However, the systems that are updated each frame (some more often) were still out of reach. These systems are otherwise known as the main/game update loop. Systems could not be disabled for performance, reordered for preference, and, most importantly, new arbitrary systems could not be added into the update loop. Of course you could always use functions like Update
, FixedUpdate
and LateUpdate
to hook into the built in update systems, but these always occurred inbetween Unity’s internal systems, beyond user control.
With Unity 2018.1, the PlayerLoop
and PlayerLoopSystem
classes and the UnityEngine.Experimental.PlayerLoop
namespace have been introduced, allowing users to remove and reorder engine update systems, as well as implement custom systems.
[[MORE]]
The Default PlayerLoop
The first step to understanding the PlayerLoop is to find a way of viewing what the out-of-the-box loop is actually made of. You can get the default loop thusly:
PlayerLoopSystem loop = PlayerLoop.GetDefaultPlayerLoop()
PlayerLoopSystem is a struct that is organized in a recursive, tree-like structure. From this object you can get some information about the system:
type
: For default systems this is aSystem.Type
object that acts as a representation of what update system this is. These types are only used as identifiers, and don’t actually contain anything. For example, the type of thePlayerLoopSystem
responsible for updating AI isUnityEngine.Experimental.PlayerLoop.AIUpdate
. We’ll use this field later to search for built-in types from the default player loop. This field is mostly useless for custom systems, except for its use as a label for the system in the Profiler.subSystemList
: As previously stated,PlayerLoopSystem
is a recursive structure, so this is an array of all of thePlayerLoopSystems
underneath this system. Traversing this array recursively will let us inspect the default loop.updateFunction
: For default systems this is a pointer to the native-side function that is executed for this update system. This field is unused for custom systems.updateDelegate
: This is a C# delegate that is primarily used for custom systems. However, it seems that it is possible to use this to add a callback to default systems as well. When we implement a custom system this is where all the work will happen.loopConditionFunction
: For default systems this is a pointer to a native-side function that is executed to check if this system and all of the systems below it should be executed. This field is unused for custom systems.
The system returned by GetDefaultPlayerLoop()
is somewhat bogus. The only field that has a value is subSystemList
. This is because this system is used as a “root node”, which every recursive structure requires. It’s fairly trivial to write a quick recursive function to pretty-print the contents of the default loop:
[RuntimeInitializeOnLoadMethod]
private static void AppStart()
{
var def = PlayerLoop.GetDefaultPlayerLoop();
var sb = new StringBuilder();
RecursivePlayerLoopPrint(def, sb, 0);
Debug.Log(sb.ToString());
}
private static void RecursivePlayerLoopPrint(PlayerLoopSystem def, StringBuilder sb, int depth)
{
if (depth == 0)
{
sb.AppendLine("ROOT NODE");
}
else if (def.type != null)
{
for (int i = 0; i < depth; i++)
{
sb.Append("\t");
}
sb.AppendLine(def.type.Name);
}
if (def.subSystemList != null)
{
depth++;
foreach (var s in def.subSystemList)
{
RecursivePlayerLoopPrint(s, sb, depth);
}
depth--;
}
}
Running this gives us a pretty large tree of systems:
ROOT NODE
Initialization
PlayerUpdateTime
AsyncUploadTimeSlicedUpdate
SynchronizeInputs
SynchronizeState
XREarlyUpdate
EarlyUpdate
PollPlayerConnection
ProfilerStartFrame
GpuTimestamp
UnityConnectClientUpdate
CloudWebServicesUpdate
UnityWebRequestUpdate
ExecuteMainThreadJobs
ProcessMouseInWindow
ClearIntermediateRenderers
ClearLines
PresentBeforeUpdate
ResetFrameStatsAfterPresent
UpdateAllUnityWebStreams
UpdateAsyncReadbackManager
UpdateTextureStreamingManager
UpdatePreloading
RendererNotifyInvisible
PlayerCleanupCachedData
UpdateMainGameViewRect
UpdateCanvasRectTransform
UpdateInputManager
ProcessRemoteInput
XRUpdate
TangoUpdate
ScriptRunDelayedStartupFrame
UpdateKinect
DeliverIosPlatformEvents
DispatchEventQueueEvents
DirectorSampleTime
PhysicsResetInterpolatedTransformPosition
NewInputBeginFrame
SpriteAtlasManagerUpdate
PerformanceAnalyticsUpdate
FixedUpdate
ClearLines
NewInputEndFixedUpdate
DirectorFixedSampleTime
AudioFixedUpdate
ScriptRunBehaviourFixedUpdate
DirectorFixedUpdate
LegacyFixedAnimationUpdate
XRFixedUpdate
PhysicsFixedUpdate
Physics2DFixedUpdate
DirectorFixedUpdatePostPhysics
ScriptRunDelayedFixedFrameRate
ScriptRunDelayedTasks
NewInputBeginFixedUpdate
PreUpdate
PhysicsUpdate
Physics2DUpdate
CheckTexFieldInput
IMGUISendQueuedEvents
NewInputUpdate
SendMouseEvents
AIUpdate
WindUpdate
UpdateVideo
Update
ScriptRunBehaviourUpdate
ScriptRunDelayedDynamicFrameRate
DirectorUpdate
PreLateUpdate
AIUpdatePostScript
DirectorUpdateAnimationBegin
LegacyAnimationUpdate
DirectorUpdateAnimationEnd
DirectorDeferredEvaluate
UpdateNetworkManager
UpdateMasterServerInterface
UNetUpdate
EndGraphicsJobsLate
ParticleSystemBeginUpdateAll
ScriptRunBehaviourLateUpdate
ConstraintManagerUpdate
PostLateUpdate
PlayerSendFrameStarted
DirectorLateUpdate
ScriptRunDelayedDynamicFrameRate
PhysicsSkinnedClothBeginUpdate
UpdateCanvasRectTransform
PlayerUpdateCanvases
UpdateAudio
ParticlesLegacyUpdateAllParticleSystems
ParticleSystemEndUpdateAll
UpdateCustomRenderTextures
UpdateAllRenderers
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
ProfilerEndFrame
A Simple Custom PlayerLoopSystem
Creating a complete replacement system is quite easy:
[RuntimeInitializeOnLoadMethod]
private static void AppStart()
{
var systemRoot = new PlayerLoopSystem();
systemRoot.subSystemList = new PlayerLoopSystem[]
{
new PlayerLoopSystem()
{
updateDelegate = CustomUpdate,
type = typeof(PlayerLoopTest)
}
};
PlayerLoop.SetPlayerLoop(systemRoot);
}
private static void CustomUpdate()
{
Debug.Log("Custom update running!");
}
A few things to take notice of: It seems that root system execution is completely ignored. If you specify a value for updateDelegate on the root system it will not be executed. This is why we need to define a root node and place our system underneath. Also note that this is a complete replacement. None of the default systems are running here. If you place a dynamic physics object in the scene it won’t move. The values in the Time class won’t be updated, and neither will input. Clearly, the default player loop is extremely sensitive to changes.
Borrowing Default Systems
Just for fun, why don’t we add one default system back into the mix? We can once again use recursion to find a default system by type and include it in our subsystem list:
[RuntimeInitializeOnLoadMethod]
private static void AppStart()
{
var defaultSystems = PlayerLoop.GetDefaultPlayerLoop();
var physicsFixedUpdateSystem = FindSubSystem<FixedUpdate.PhysicsFixedUpdate>(defaultSystems);
var systemRoot = new PlayerLoopSystem();
systemRoot.subSystemList = new PlayerLoopSystem[]
{
physicsFixedUpdateSystem,
new PlayerLoopSystem()
{
updateDelegate = CustomUpdate,
type = typeof(PlayerLoopTest)
},
};
PlayerLoop.SetPlayerLoop(systemRoot);
}
private static void CustomUpdate()
{
Debug.Log("Custom update running!");
}
private static PlayerLoopSystem FindSubSystem<T>(PlayerLoopSystem def)
{
if (def.type == typeof(T))
{
return def;
}
if (def.subSystemList != null)
{
foreach (var s in def.subSystemList)
{
var system = FindSubSystem(s, type);
if (system.type == typeof(T))
{
return system;
}
}
}
return default(PlayerLoopSystem);
}
There’s more efficiency to be gained here if we’re looking for multiple systems by type, but this works for now. You’ll notice that this creates incorrect behavior; physics forces are way too powerful! That’s because we’re updating physics on a framerate dependent update loop instead of on a fixed time update loop. The FixedUpdate PlayerLoopSystem
handles timing and using correct delta times for all of the subsystems beneath it, which we aren’t doing here. Fixing this would be both daunting and freeing; you could implement your own timestep! We won’t be covering that here, though.
Replacing a Default System
You may have read the 10000 Update() calls article on the official Unity blog. In this article the author discusses implementing a managed-side custom update loop as a replacement for the Update
call. We can do this better by actually replacing the default Update
call, which was printed in our list as Update.ScriptRunBehaviourUpdate
. We can modify our previous function to replace the system we found by type with our own system, maintaining the execution order. However, PlayerLoopSystem
is a struct, and will be passed by value into our function. In order to modify what we pass in, we’ll use the ref
keyword:
[RuntimeInitializeOnLoadMethod]
private static void AppStart()
{
var defaultSystems = PlayerLoop.GetDefaultPlayerLoop();
var customUpdate = new PlayerLoopSystem()
{
updateDelegate = CustomUpdate,
type = typeof(PlayerLoopTest)
};
ReplaceSystem<Update.ScriptRunBehaviourUpdate>(ref defaultSystems, customUpdate);
PlayerLoop.SetPlayerLoop(defaultSystems);
}
private static void CustomUpdate()
{
Debug.Log("Custom update running!");
}
private static bool ReplaceSystem<T>(ref PlayerLoopSystem system, PlayerLoopSystem replacement)
{
if (system.type == typeof(T))
{
system = replacement;
return true;
}
if (system.subSystemList != null)
{
for (var i = 0; i < system.subSystemList.Length; i++)
{
if (ReplaceSystem(system.subSystemList[i], replacement, toReplace))
{
return true;
}
}
}
return false;
}
If you create a new script with an Update()
call and add it to an object in your scene, you’ll notice it won’t be called anymore. Note that this example doesn’t cover the other required steps to actually replace all of the functionality of Update()
, such as creating an object management system to add and remove updatable objects from a global collection and calling update functions on them. The ideal implementation would probably use an IUpdatable
interface to allow nearly any object to be included in the custom update loop (and eliminate the need for “magic methods”).
…And More
There’s certainly more to be experimented with this wonderful new access to the low level systems that literally makes Unity tick. Hopefully this post gives you a good head start to shaping Unity to fit your needs. A few quick ideas as to interesting additions that you can try to add as utility API:
- Inserting systems. Some modifications to the replacement example could allow you to insert a system into a subsystem array instead of replacing it.
- Explore different ways of disabling systems temporarily. If your game is in a pause menu you don’t need AI updates running!
- Create a visualizer. This could be in-game or in-editor. The Profiler does list every system using the
PlayerLoopSystem
type field as a label, but knowing in a debug build what systems are currently enabled could be very beneficial when doing heavy customization to the default PlayerLoop. - High performance update loops. This could be used for mobile games that don’t need systems like physics, AI or XR.
Let me know what you think on Twitter or Reddit about this new API, how you might use it for your games, and what else you’re looking for in the Unity application lifecycle.