SpatialHashingInterestManagement.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. // extremely fast spatial hashing interest management based on uMMORPG GridChecker.
  2. // => 30x faster in initial tests
  3. // => scales way higher
  4. using System.Collections.Generic;
  5. using UnityEngine;
  6. namespace Mirror
  7. {
  8. [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")]
  9. public class SpatialHashingInterestManagement : InterestManagement
  10. {
  11. [Tooltip("The maximum range that objects will be visible at.")]
  12. public int visRange = 30;
  13. // we use a 9 neighbour grid.
  14. // so we always see in a distance of 2 grids.
  15. // for example, our own grid and then one on top / below / left / right.
  16. //
  17. // this means that grid resolution needs to be distance / 2.
  18. // so for example, for distance = 30 we see 2 cells = 15 * 2 distance.
  19. //
  20. // on first sight, it seems we need distance / 3 (we see left/us/right).
  21. // but that's not the case.
  22. // resolution would be 10, and we only see 1 cell far, so 10+10=20.
  23. public int resolution => visRange / 2;
  24. [Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
  25. public float rebuildInterval = 1;
  26. double lastRebuildTime;
  27. public enum CheckMethod
  28. {
  29. XZ_FOR_3D,
  30. XY_FOR_2D
  31. }
  32. [Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")]
  33. public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D;
  34. // debugging
  35. public bool showSlider;
  36. // the grid
  37. // begin with a large capacity to avoid resizing & allocations.
  38. Grid2D<NetworkConnectionToClient> grid = new Grid2D<NetworkConnectionToClient>(1024);
  39. // project 3d world position to grid position
  40. Vector2Int ProjectToGrid(Vector3 position) =>
  41. checkMethod == CheckMethod.XZ_FOR_3D
  42. ? Vector2Int.RoundToInt(new Vector2(position.x, position.z) / resolution)
  43. : Vector2Int.RoundToInt(new Vector2(position.x, position.y) / resolution);
  44. public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
  45. {
  46. // calculate projected positions
  47. Vector2Int projected = ProjectToGrid(identity.transform.position);
  48. Vector2Int observerProjected = ProjectToGrid(newObserver.identity.transform.position);
  49. // distance needs to be at max one of the 8 neighbors, which is
  50. // 1 for the direct neighbors
  51. // 1.41 for the diagonal neighbors (= sqrt(2))
  52. // => use sqrMagnitude and '2' to avoid computations. same result.
  53. return (projected - observerProjected).sqrMagnitude <= 2;
  54. }
  55. public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
  56. {
  57. // add everyone in 9 neighbour grid
  58. // -> pass observers to GetWithNeighbours directly to avoid allocations
  59. // and expensive .UnionWith computations.
  60. Vector2Int current = ProjectToGrid(identity.transform.position);
  61. grid.GetWithNeighbours(current, newObservers);
  62. }
  63. [ServerCallback]
  64. public override void Reset()
  65. {
  66. lastRebuildTime = 0D;
  67. }
  68. // update everyone's position in the grid
  69. // (internal so we can update from tests)
  70. [ServerCallback]
  71. internal void Update()
  72. {
  73. // NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL
  74. // entities every INTERVAL. consider the other approach later.
  75. // IMPORTANT: refresh grid every update!
  76. // => newly spawned entities get observers assigned via
  77. // OnCheckObservers. this can happen any time and we don't want
  78. // them broadcast to old (moved or destroyed) connections.
  79. // => players do move all the time. we want them to always be in the
  80. // correct grid position.
  81. // => note that the actual 'rebuildall' doesn't need to happen all
  82. // the time.
  83. // NOTE: consider refreshing grid only every 'interval' too. but not
  84. // for now. stability & correctness matter.
  85. // clear old grid results before we update everyone's position.
  86. // (this way we get rid of destroyed connections automatically)
  87. //
  88. // NOTE: keeps allocated HashSets internally.
  89. // clearing & populating every frame works without allocations
  90. grid.ClearNonAlloc();
  91. // put every connection into the grid at it's main player's position
  92. // NOTE: player sees in a radius around him. NOT around his pet too.
  93. foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values)
  94. {
  95. // authenticated and joined world with a player?
  96. if (connection.isAuthenticated && connection.identity != null)
  97. {
  98. // calculate current grid position
  99. Vector2Int position = ProjectToGrid(connection.identity.transform.position);
  100. // put into grid
  101. grid.Add(position, connection);
  102. }
  103. }
  104. // rebuild all spawned entities' observers every 'interval'
  105. // this will call OnRebuildObservers which then returns the
  106. // observers at grid[position] for each entity.
  107. if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
  108. {
  109. RebuildAll();
  110. lastRebuildTime = NetworkTime.localTime;
  111. }
  112. }
  113. // OnGUI allocates even if it does nothing. avoid in release.
  114. #if UNITY_EDITOR || DEVELOPMENT_BUILD
  115. // slider from dotsnet. it's nice to play around with in the benchmark
  116. // demo.
  117. void OnGUI()
  118. {
  119. if (!showSlider) return;
  120. // only show while server is running. not on client, etc.
  121. if (!NetworkServer.active) return;
  122. int height = 30;
  123. int width = 250;
  124. GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height));
  125. GUILayout.BeginHorizontal("Box");
  126. GUILayout.Label("Radius:");
  127. visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150)));
  128. GUILayout.Label(visRange.ToString());
  129. GUILayout.EndHorizontal();
  130. GUILayout.EndArea();
  131. }
  132. #endif
  133. }
  134. }