diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkUpdateLoop.cs b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkUpdateLoop.cs index 1ef881a18f..7eee1143b6 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkUpdateLoop.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkUpdateLoop.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Collections.Generic; using UnityEngine; using UnityEngine.LowLevel; @@ -234,131 +233,166 @@ public static PlayerLoopSystem CreateLoopSystem() } } - [RuntimeInitializeOnLoadMethod] + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void Initialize() { - var customPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + UnregisterLoopSystems(); + RegisterLoopSystems(); + } - for (int i = 0; i < customPlayerLoop.subSystemList.Length; i++) - { - var playerLoopSystem = customPlayerLoop.subSystemList[i]; + private enum LoopSystemPosition + { + After, + Before + } - if (playerLoopSystem.type == typeof(Initialization)) + private static bool TryAddLoopSystem(ref PlayerLoopSystem parentLoopSystem, PlayerLoopSystem childLoopSystem, Type anchorSystemType, LoopSystemPosition loopSystemPosition) + { + int systemPosition = -1; + if (anchorSystemType != null) + { + for (int i = 0; i < parentLoopSystem.subSystemList.Length; i++) { - var subsystems = playerLoopSystem.subSystemList.ToList(); + var subsystem = parentLoopSystem.subSystemList[i]; + if (subsystem.type == anchorSystemType) { - // insert at the bottom of `Initialization` - subsystems.Add(NetworkInitialization.CreateLoopSystem()); + systemPosition = loopSystemPosition == LoopSystemPosition.After ? i + 1 : i; + break; } - playerLoopSystem.subSystemList = subsystems.ToArray(); } - else if (playerLoopSystem.type == typeof(EarlyUpdate)) + } + else + { + systemPosition = loopSystemPosition == LoopSystemPosition.After ? parentLoopSystem.subSystemList.Length : 0; + } + + if (systemPosition == -1) return false; + + var newSubsystemList = new PlayerLoopSystem[parentLoopSystem.subSystemList.Length + 1]; + + // begin = systemsBefore + systemsAfter + // + systemsBefore + if (systemPosition > 0) Array.Copy(parentLoopSystem.subSystemList, newSubsystemList, systemPosition); + // + childSystem + newSubsystemList[systemPosition] = childLoopSystem; + // + systemsAfter + if (systemPosition < parentLoopSystem.subSystemList.Length) Array.Copy(parentLoopSystem.subSystemList, systemPosition, newSubsystemList, systemPosition + 1, parentLoopSystem.subSystemList.Length - systemPosition); + // end = systemsBefore + childSystem + systemsAfter + + parentLoopSystem.subSystemList = newSubsystemList; + + return true; + } + + private static bool TryRemoveLoopSystem(ref PlayerLoopSystem parentLoopSystem, Type childSystemType) + { + int systemPosition = -1; + for (int i = 0; i < parentLoopSystem.subSystemList.Length; i++) + { + var subsystem = parentLoopSystem.subSystemList[i]; + if (subsystem.type == childSystemType) { - var subsystems = playerLoopSystem.subSystemList.ToList(); - { - int subsystemCount = subsystems.Count; - for (int k = 0; k < subsystemCount; k++) - { - if (subsystems[k].type == typeof(EarlyUpdate.ScriptRunDelayedStartupFrame)) - { - // insert before `EarlyUpdate.ScriptRunDelayedStartupFrame` - subsystems.Insert(k, NetworkEarlyUpdate.CreateLoopSystem()); - break; - } - } - } - playerLoopSystem.subSystemList = subsystems.ToArray(); + systemPosition = i; + break; } - else if (playerLoopSystem.type == typeof(FixedUpdate)) + } + + if (systemPosition == -1) return false; + + var newSubsystemList = new PlayerLoopSystem[parentLoopSystem.subSystemList.Length - 1]; + + // begin = systemsBefore + childSystem + systemsAfter + // + systemsBefore + if (systemPosition > 0) Array.Copy(parentLoopSystem.subSystemList, newSubsystemList, systemPosition); + // + systemsAfter + if (systemPosition < parentLoopSystem.subSystemList.Length - 1) Array.Copy(parentLoopSystem.subSystemList, systemPosition + 1, newSubsystemList, systemPosition, parentLoopSystem.subSystemList.Length - systemPosition - 1); + // end = systemsBefore + systemsAfter + + parentLoopSystem.subSystemList = newSubsystemList; + + return true; + } + + internal static void RegisterLoopSystems() + { + var rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + for (int i = 0; i < rootPlayerLoop.subSystemList.Length; i++) + { + ref var currentSystem = ref rootPlayerLoop.subSystemList[i]; + + if (currentSystem.type == typeof(Initialization)) { - var subsystems = playerLoopSystem.subSystemList.ToList(); - { - int subsystemCount = subsystems.Count; - for (int k = 0; k < subsystemCount; k++) - { - if (subsystems[k].type == typeof(FixedUpdate.ScriptRunBehaviourFixedUpdate)) - { - // insert before `FixedUpdate.ScriptRunBehaviourFixedUpdate` - subsystems.Insert(k, NetworkFixedUpdate.CreateLoopSystem()); - break; - } - } - } - playerLoopSystem.subSystemList = subsystems.ToArray(); + TryAddLoopSystem(ref currentSystem, NetworkInitialization.CreateLoopSystem(), null, LoopSystemPosition.After); } - else if (playerLoopSystem.type == typeof(PreUpdate)) + else if (currentSystem.type == typeof(EarlyUpdate)) { - var subsystems = playerLoopSystem.subSystemList.ToList(); - { - int subsystemCount = subsystems.Count; - for (int k = 0; k < subsystemCount; k++) - { - if (subsystems[k].type == typeof(PreUpdate.PhysicsUpdate)) - { - // insert before `PreUpdate.PhysicsUpdate` - subsystems.Insert(k, NetworkPreUpdate.CreateLoopSystem()); - break; - } - } - } - playerLoopSystem.subSystemList = subsystems.ToArray(); + TryAddLoopSystem(ref currentSystem, NetworkEarlyUpdate.CreateLoopSystem(), typeof(EarlyUpdate.ScriptRunDelayedStartupFrame), LoopSystemPosition.Before); } - else if (playerLoopSystem.type == typeof(Update)) + else if (currentSystem.type == typeof(FixedUpdate)) { - var subsystems = playerLoopSystem.subSystemList.ToList(); - { - int subsystemCount = subsystems.Count; - for (int k = 0; k < subsystemCount; k++) - { - if (subsystems[k].type == typeof(Update.ScriptRunBehaviourUpdate)) - { - // insert before `Update.ScriptRunBehaviourUpdate` - subsystems.Insert(k, NetworkUpdate.CreateLoopSystem()); - break; - } - } - } - playerLoopSystem.subSystemList = subsystems.ToArray(); + TryAddLoopSystem(ref currentSystem, NetworkFixedUpdate.CreateLoopSystem(), typeof(FixedUpdate.ScriptRunBehaviourFixedUpdate), LoopSystemPosition.Before); } - else if (playerLoopSystem.type == typeof(PreLateUpdate)) + else if (currentSystem.type == typeof(PreUpdate)) { - var subsystems = playerLoopSystem.subSystemList.ToList(); - { - int subsystemCount = subsystems.Count; - for (int k = 0; k < subsystemCount; k++) - { - if (subsystems[k].type == typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate)) - { - // insert before `PreLateUpdate.ScriptRunBehaviourLateUpdate` - subsystems.Insert(k, NetworkPreLateUpdate.CreateLoopSystem()); - break; - } - } - } - playerLoopSystem.subSystemList = subsystems.ToArray(); + TryAddLoopSystem(ref currentSystem, NetworkPreUpdate.CreateLoopSystem(), typeof(PreUpdate.PhysicsUpdate), LoopSystemPosition.Before); } - else if (playerLoopSystem.type == typeof(PostLateUpdate)) + else if (currentSystem.type == typeof(Update)) { - var subsystems = playerLoopSystem.subSystemList.ToList(); - { - int subsystemCount = subsystems.Count; - for (int k = 0; k < subsystemCount; k++) - { - if (subsystems[k].type == typeof(PostLateUpdate.PlayerSendFrameComplete)) - { - // insert after `PostLateUpdate.PlayerSendFrameComplete` - subsystems.Insert(k + 1, NetworkPostLateUpdate.CreateLoopSystem()); - break; - } - } - } - playerLoopSystem.subSystemList = subsystems.ToArray(); + TryAddLoopSystem(ref currentSystem, NetworkUpdate.CreateLoopSystem(), typeof(Update.ScriptRunBehaviourUpdate), LoopSystemPosition.Before); } + else if (currentSystem.type == typeof(PreLateUpdate)) + { + TryAddLoopSystem(ref currentSystem, NetworkPreLateUpdate.CreateLoopSystem(), typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate), LoopSystemPosition.Before); + } + else if (currentSystem.type == typeof(PostLateUpdate)) + { + TryAddLoopSystem(ref currentSystem, NetworkPostLateUpdate.CreateLoopSystem(), typeof(PostLateUpdate.PlayerSendFrameComplete), LoopSystemPosition.After); + } + } + + PlayerLoop.SetPlayerLoop(rootPlayerLoop); + } + + internal static void UnregisterLoopSystems() + { + var rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + for (int i = 0; i < rootPlayerLoop.subSystemList.Length; i++) + { + ref var currentSystem = ref rootPlayerLoop.subSystemList[i]; - customPlayerLoop.subSystemList[i] = playerLoopSystem; + if (currentSystem.type == typeof(Initialization)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkInitialization)); + } + else if (currentSystem.type == typeof(EarlyUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkEarlyUpdate)); + } + else if (currentSystem.type == typeof(FixedUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkFixedUpdate)); + } + else if (currentSystem.type == typeof(PreUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPreUpdate)); + } + else if (currentSystem.type == typeof(Update)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkUpdate)); + } + else if (currentSystem.type == typeof(PreLateUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPreLateUpdate)); + } + else if (currentSystem.type == typeof(PostLateUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPostLateUpdate)); + } } - PlayerLoop.SetPlayerLoop(customPlayerLoop); + PlayerLoop.SetPlayerLoop(rootPlayerLoop); } } -} +} \ No newline at end of file diff --git a/com.unity.multiplayer.mlapi/Tests/Runtime/NetworkUpdateLoopTests.cs b/com.unity.multiplayer.mlapi/Tests/Runtime/NetworkUpdateLoopTests.cs index 187b037088..ad796532ac 100644 --- a/com.unity.multiplayer.mlapi/Tests/Runtime/NetworkUpdateLoopTests.cs +++ b/com.unity.multiplayer.mlapi/Tests/Runtime/NetworkUpdateLoopTests.cs @@ -12,7 +12,74 @@ namespace MLAPI.RuntimeTests public class NetworkUpdateLoopTests { [Test] - public void UpdateStageInjection() + public void RegisterCustomLoopInTheMiddle() + { + // caching the current PlayerLoop (to prevent side-effects on other tests) + var cachedPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + { + // since current PlayerLoop already took NetworkUpdateLoop systems inside, + // we are going to swap it with the default PlayerLoop temporarily for testing + PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop()); + + NetworkUpdateLoop.RegisterLoopSystems(); + + var curPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + int initSubsystemCount = curPlayerLoop.subSystemList[0].subSystemList.Length; + var newInitSubsystems = new PlayerLoopSystem[initSubsystemCount + 1]; + Array.Copy(curPlayerLoop.subSystemList[0].subSystemList, newInitSubsystems, initSubsystemCount); + newInitSubsystems[initSubsystemCount] = new PlayerLoopSystem { type = typeof(NetworkUpdateLoopTests) }; + curPlayerLoop.subSystemList[0].subSystemList = newInitSubsystems; + PlayerLoop.SetPlayerLoop(curPlayerLoop); + + NetworkUpdateLoop.UnregisterLoopSystems(); + + // our custom `PlayerLoopSystem` with the type of `NetworkUpdateLoopTests` should still exist + Assert.AreEqual(typeof(NetworkUpdateLoopTests), PlayerLoop.GetCurrentPlayerLoop().subSystemList[0].subSystemList.Last().type); + } + // replace the current PlayerLoop with the cached PlayerLoop after the test + PlayerLoop.SetPlayerLoop(cachedPlayerLoop); + } + + [UnityTest] + public IEnumerator RegisterAndUnregisterSystems() + { + // caching the current PlayerLoop (it will have NetworkUpdateLoop systems registered) + var cachedPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + { + // since current PlayerLoop already took NetworkUpdateLoop systems inside, + // we are going to swap it with the default PlayerLoop temporarily for testing + PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop()); + + var oldPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + NetworkUpdateLoop.RegisterLoopSystems(); + + int nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + NetworkUpdateLoop.UnregisterLoopSystems(); + + var newPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + // recursively compare old and new PlayerLoop systems and their subsystems + AssertAreEqualPlayerLoopSystems(newPlayerLoop, oldPlayerLoop); + } + // replace the current PlayerLoop with the cached PlayerLoop after the test + PlayerLoop.SetPlayerLoop(cachedPlayerLoop); + } + + private void AssertAreEqualPlayerLoopSystems(PlayerLoopSystem leftPlayerLoop, PlayerLoopSystem rightPlayerLoop) + { + Assert.AreEqual(leftPlayerLoop.type, rightPlayerLoop.type); + Assert.AreEqual(leftPlayerLoop.subSystemList?.Length ?? 0, rightPlayerLoop.subSystemList?.Length ?? 0); + for (int i = 0; i < (leftPlayerLoop.subSystemList?.Length ?? 0); i++) + { + AssertAreEqualPlayerLoopSystems(leftPlayerLoop.subSystemList[i], rightPlayerLoop.subSystemList[i]); + } + } + + [Test] + public void UpdateStageSystems() { var currentPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); for (int i = 0; i < currentPlayerLoop.subSystemList.Length; i++) @@ -369,4 +436,4 @@ public IEnumerator UpdateStagesMixed() } } } -} +} \ No newline at end of file