NetworkLoop.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // our ideal update looks like this:
  2. // transport.process_incoming()
  3. // update_world()
  4. // transport.process_outgoing()
  5. //
  6. // this way we avoid unnecessary latency for low-ish server tick rates.
  7. // for example, if we were to use this tick:
  8. // transport.process_incoming/outgoing()
  9. // update_world()
  10. //
  11. // then anything sent in update_world wouldn't be actually sent out by the
  12. // transport until the next frame. if server runs at 60Hz, then this can add
  13. // 16ms latency for every single packet.
  14. //
  15. // => instead we process incoming, update world, process_outgoing in the same
  16. // frame. it's more clear (no race conditions) and lower latency.
  17. // => we need to add custom Update functions to the Unity engine:
  18. // NetworkEarlyUpdate before Update()/FixedUpdate()
  19. // NetworkLateUpdate after LateUpdate()
  20. // this way the user can update the world in Update/FixedUpdate/LateUpdate
  21. // and networking still runs before/after those functions no matter what!
  22. // => see also: https://docs.unity3d.com/Manual/ExecutionOrder.html
  23. // => update order:
  24. // * we add to the end of EarlyUpdate so it runs after any Unity initializations
  25. // * we add to the end of PreLateUpdate so it runs after LateUpdate(). adding
  26. // to the beginning of PostLateUpdate doesn't actually work.
  27. using System;
  28. using UnityEngine;
  29. using UnityEngine.LowLevel;
  30. using UnityEngine.PlayerLoop;
  31. namespace Mirror
  32. {
  33. public static class NetworkLoop
  34. {
  35. // helper enum to add loop to begin/end of subSystemList
  36. internal enum AddMode { Beginning, End }
  37. // callbacks for others to hook into if they need Early/LateUpdate.
  38. public static Action OnEarlyUpdate;
  39. public static Action OnLateUpdate;
  40. // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
  41. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
  42. static void ResetStatics()
  43. {
  44. OnEarlyUpdate = null;
  45. OnLateUpdate = null;
  46. }
  47. // helper function to find an update function's index in a player loop
  48. // type. this is used for testing to guarantee our functions are added
  49. // at the beginning/end properly.
  50. internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction function, PlayerLoopSystem playerLoop, Type playerLoopSystemType)
  51. {
  52. // did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc.
  53. if (playerLoop.type == playerLoopSystemType)
  54. return Array.FindIndex(playerLoop.subSystemList, (elem => elem.updateDelegate == function));
  55. // recursively keep looking
  56. if (playerLoop.subSystemList != null)
  57. {
  58. for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
  59. {
  60. int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType);
  61. if (index != -1) return index;
  62. }
  63. }
  64. return -1;
  65. }
  66. // MODIFIED AddSystemToPlayerLoopList from Unity.Entities.ScriptBehaviourUpdateOrder (ECS)
  67. //
  68. // => adds an update function to the Unity internal update type.
  69. // => Unity has different update loops:
  70. // https://medium.com/@thebeardphantom/unity-2018-and-playerloop-5c46a12a677
  71. // EarlyUpdate
  72. // FixedUpdate
  73. // PreUpdate
  74. // Update
  75. // PreLateUpdate
  76. // PostLateUpdate
  77. //
  78. // function: the custom update function to add
  79. // IMPORTANT: according to a comment in Unity.Entities.ScriptBehaviourUpdateOrder,
  80. // the UpdateFunction can not be virtual because
  81. // Mono 4.6 has problems invoking virtual methods
  82. // as delegates from native!
  83. // ownerType: the .type to fill in so it's obvious who the new function
  84. // belongs to. seems to be mostly for debugging. pass any.
  85. // addMode: prepend or append to update list
  86. internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, Type ownerType, ref PlayerLoopSystem playerLoop, Type playerLoopSystemType, AddMode addMode)
  87. {
  88. // did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc.
  89. if (playerLoop.type == playerLoopSystemType)
  90. {
  91. // debugging
  92. //Debug.Log($"Found playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:");
  93. //foreach (PlayerLoopSystem sys in playerLoop.subSystemList)
  94. // Debug.Log($" ->{sys.type}");
  95. // make sure the function wasn't added yet.
  96. // with domain reload disabled, it would otherwise be added twice:
  97. // fixes: https://github.com/MirrorNetworking/Mirror/issues/3392
  98. if (Array.FindIndex(playerLoop.subSystemList, (s => s.updateDelegate == function)) != -1)
  99. {
  100. // loop contains the function, so return true.
  101. return true;
  102. }
  103. // resize & expand subSystemList to fit one more entry
  104. int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0;
  105. Array.Resize(ref playerLoop.subSystemList, oldListLength + 1);
  106. // IMPORTANT: always insert a FRESH PlayerLoopSystem!
  107. // We CAN NOT resize and then OVERWRITE an entry's type/loop.
  108. // => PlayerLoopSystem has native IntPtr loop members
  109. // => forgetting to clear those would cause undefined behaviour!
  110. // see also: https://github.com/vis2k/Mirror/pull/2652
  111. PlayerLoopSystem system = new PlayerLoopSystem {
  112. type = ownerType,
  113. updateDelegate = function
  114. };
  115. // prepend our custom loop to the beginning
  116. if (addMode == AddMode.Beginning)
  117. {
  118. // shift to the right, write into first array element
  119. Array.Copy(playerLoop.subSystemList, 0, playerLoop.subSystemList, 1, playerLoop.subSystemList.Length - 1);
  120. playerLoop.subSystemList[0] = system;
  121. }
  122. // append our custom loop to the end
  123. else if (addMode == AddMode.End)
  124. {
  125. // simply write into last array element
  126. playerLoop.subSystemList[oldListLength] = system;
  127. }
  128. // debugging
  129. //Debug.Log($"New playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:");
  130. //foreach (PlayerLoopSystem sys in playerLoop.subSystemList)
  131. // Debug.Log($" ->{sys.type}");
  132. return true;
  133. }
  134. // recursively keep looking
  135. if (playerLoop.subSystemList != null)
  136. {
  137. for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
  138. {
  139. if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode))
  140. return true;
  141. }
  142. }
  143. return false;
  144. }
  145. // hook into Unity runtime to actually add our custom functions
  146. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
  147. static void RuntimeInitializeOnLoad()
  148. {
  149. //Debug.Log("Mirror: adding Network[Early/Late]Update to Unity...");
  150. // get loop
  151. // 2019 has GetCURRENTPlayerLoop which is safe to use without
  152. // breaking other custom system's custom loops.
  153. // see also: https://github.com/vis2k/Mirror/pull/2627/files
  154. PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop();
  155. // add NetworkEarlyUpdate to the end of EarlyUpdate so it runs after
  156. // any Unity initializations but before the first Update/FixedUpdate
  157. AddToPlayerLoop(NetworkEarlyUpdate, typeof(NetworkLoop), ref playerLoop, typeof(EarlyUpdate), AddMode.End);
  158. // add NetworkLateUpdate to the end of PreLateUpdate so it runs after
  159. // LateUpdate(). adding to the beginning of PostLateUpdate doesn't
  160. // actually work.
  161. AddToPlayerLoop(NetworkLateUpdate, typeof(NetworkLoop), ref playerLoop, typeof(PreLateUpdate), AddMode.End);
  162. // set the new loop
  163. PlayerLoop.SetPlayerLoop(playerLoop);
  164. }
  165. static void NetworkEarlyUpdate()
  166. {
  167. // loop functions run in edit mode and in play mode.
  168. // however, we only want to call NetworkServer/Client in play mode.
  169. if (!Application.isPlaying) return;
  170. NetworkTime.EarlyUpdate();
  171. //Debug.Log($"NetworkEarlyUpdate {Time.time}");
  172. NetworkServer.NetworkEarlyUpdate();
  173. NetworkClient.NetworkEarlyUpdate();
  174. // invoke event after mirror has done it's early updating.
  175. OnEarlyUpdate?.Invoke();
  176. }
  177. static void NetworkLateUpdate()
  178. {
  179. // loop functions run in edit mode and in play mode.
  180. // however, we only want to call NetworkServer/Client in play mode.
  181. if (!Application.isPlaying) return;
  182. //Debug.Log($"NetworkLateUpdate {Time.time}");
  183. // invoke event before mirror does its final late updating.
  184. OnLateUpdate?.Invoke();
  185. NetworkServer.NetworkLateUpdate();
  186. NetworkClient.NetworkLateUpdate();
  187. }
  188. }
  189. }