From fe9b469a3d9d091a7de0297ab51947820c31ea30 Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Thu, 17 Apr 2025 06:21:40 -0400 Subject: [PATCH] perf(Spatial Hash IM): Refresh Grid on Interval - Substantial performance increase at scale - Notes added to explain why this is acceptable - Default visRange set to more likely real world value - Tests updated --- .../SpatialHashing3DInterestManagement.cs | 42 ++++++++++++------- .../SpatialHashingInterestManagement.cs | 42 ++++++++++++------- .../InterestManagementTests_SpatialHashing.cs | 8 ++-- 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs index 9034252f94c..ee4b277e018 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs @@ -11,19 +11,19 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")] public class SpatialHashing3DInterestManagement : InterestManagement { - [Tooltip("The maximum range that objects will be visible at.")] - public int visRange = 30; + [Tooltip("The maximum range that objects will be visible.\nSet to 10-20% larger than camera far clip plane")] + public int visRange = 1200; // we use a 9 neighbour grid. // so we always see in a distance of 2 grids. // for example, our own grid and then one on top / below / left / right. // // this means that grid resolution needs to be distance / 2. - // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // so for example, for distance = 1200 we see 2 cells = 600 * 2 distance. // // on first sight, it seems we need distance / 3 (we see left/us/right). // but that's not the case. - // resolution would be 10, and we only see 1 cell far, so 10+10=20. + // resolution would be 400, and we only see 1 cell far, so 400+400=800. public int resolution => visRange / 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] @@ -77,7 +77,7 @@ internal void Update() // NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL // entities every INTERVAL. consider the other approach later. - // IMPORTANT: refresh grid every update! + // Old Notes: refresh grid every update! // => newly spawned entities get observers assigned via // OnCheckObservers. this can happen any time and we don't want // them broadcast to old (moved or destroyed) connections. @@ -85,14 +85,33 @@ internal void Update() // correct grid position. // => note that the actual 'rebuildall' doesn't need to happen all // the time. - // NOTE: consider refreshing grid only every 'interval' too. but not - // for now. stability & correctness matter. + + // Updated Notes: refresh grid and RebuildAll every interval. + // Real world application would have visRange larger than camera + // far clip plane, e.g. 1200, and a player movement speed of ~10m/sec + // so they typically won't cross cell boundaries quickly. If users notice + // flickering or mis-positioning, they can decrease the interval. // clear old grid results before we update everyone's position. // (this way we get rid of destroyed connections automatically) // // NOTE: keeps allocated HashSets internally. // clearing & populating every frame works without allocations + + // rebuild all spawned entities' observers every 'interval' + // this will call OnRebuildObservers which then returns the + // observers at grid[position] for each entity. + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RefreshGrid(); + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + } + + // (internal so we can update from tests) + internal void RefreshGrid() + { grid.ClearNonAlloc(); // put every connection into the grid at it's main player's position @@ -109,15 +128,6 @@ internal void Update() grid.Add(position, connection); } } - - // rebuild all spawned entities' observers every 'interval' - // this will call OnRebuildObservers which then returns the - // observers at grid[position] for each entity. - if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) - { - RebuildAll(); - lastRebuildTime = NetworkTime.localTime; - } } #if !UNITY_SERVER && DEBUG diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs index 912c995e4ed..9631f6ed904 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -11,19 +11,19 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")] public class SpatialHashingInterestManagement : InterestManagement { - [Tooltip("The maximum range that objects will be visible at.")] - public int visRange = 30; + [Tooltip("The maximum range that objects will be visible.\nSet to 10-20% larger than camera far clip plane")] + public int visRange = 1200; // we use a 9 neighbour grid. // so we always see in a distance of 2 grids. // for example, our own grid and then one on top / below / left / right. // // this means that grid resolution needs to be distance / 2. - // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // so for example, for distance = 1200 we see 2 cells = 600 * 2 distance. // // on first sight, it seems we need distance / 3 (we see left/us/right). // but that's not the case. - // resolution would be 10, and we only see 1 cell far, so 10+10=20. + // resolution would be 400, and we only see 1 cell far, so 400+400=800. public int resolution => visRange / 2; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] @@ -87,7 +87,7 @@ internal void Update() // NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL // entities every INTERVAL. consider the other approach later. - // IMPORTANT: refresh grid every update! + // Old Notes: refresh grid every update! // => newly spawned entities get observers assigned via // OnCheckObservers. this can happen any time and we don't want // them broadcast to old (moved or destroyed) connections. @@ -95,14 +95,33 @@ internal void Update() // correct grid position. // => note that the actual 'rebuildall' doesn't need to happen all // the time. - // NOTE: consider refreshing grid only every 'interval' too. but not - // for now. stability & correctness matter. + + // Updated Notes: refresh grid and RebuildAll every interval. + // Real world application would have visRange larger than camera + // far clip plane, e.g. 1200, and a player movement speed of ~10m/sec + // so they typically won't cross cell boundaries quickly. If users notice + // flickering or mis-positioning, they can decrease the interval. // clear old grid results before we update everyone's position. // (this way we get rid of destroyed connections automatically) // // NOTE: keeps allocated HashSets internally. // clearing & populating every frame works without allocations + + // rebuild all spawned entities' observers every 'interval' + // this will call OnRebuildObservers which then returns the + // observers at grid[position] for each entity. + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RefreshGrid(); + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + } + + // (internal so we can update from tests) + internal void RefreshGrid() + { grid.ClearNonAlloc(); // put every connection into the grid at it's main player's position @@ -119,15 +138,6 @@ internal void Update() grid.Add(position, connection); } } - - // rebuild all spawned entities' observers every 'interval' - // this will call OnRebuildObservers which then returns the - // observers at grid[position] for each entity. - if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) - { - RebuildAll(); - lastRebuildTime = NetworkTime.localTime; - } } #if !UNITY_SERVER && DEBUG diff --git a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs index 9a708ed211e..7d5501c8a86 100644 --- a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs +++ b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs @@ -117,8 +117,8 @@ public void OutOfRange_Initial() // A and B are too far from each other identityB.transform.position = Vector3.right * (aoi.visRange + 1); - // update grid now that positions were changed - aoi.Update(); + // Refresh the grid + aoi.RefreshGrid(); // rebuild for boths NetworkServer.RebuildObservers(identityA, true); @@ -137,8 +137,8 @@ public void OutOfRange_NotInitial() // A and B are too far from each other identityB.transform.position = Vector3.right * (aoi.visRange + 1); - // update grid now that positions were changed - aoi.Update(); + // Refresh the grid + aoi.RefreshGrid(); // rebuild for boths NetworkServer.RebuildObservers(identityA, false);