SpatialHashingInterestManagement.cs 5.9 KB

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