-
-
Notifications
You must be signed in to change notification settings - Fork 742
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: SpatialHashing3D for XYZ to include vertical axis in checks (#…
…3814) * Grid3D * comment * feature: SpatialHashing3D for XYZ to include vertical axis in checks --------- Co-authored-by: mischa <16416509+vis2k@users.noreply.github.com>
- Loading branch information
Showing
7 changed files
with
346 additions
and
0 deletions.
There are no files selected for viewing
106 changes: 106 additions & 0 deletions
106
Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// Grid3D based on Grid2D | ||
// -> not named 'Grid' because Unity already has a Grid type. causes warnings. | ||
// -> struct to avoid memory indirection. it's accessed a lot. | ||
using System.Collections.Generic; | ||
using UnityEngine; | ||
|
||
namespace Mirror | ||
{ | ||
// struct to avoid memory indirection. it's accessed a lot. | ||
public struct Grid3D<T> | ||
{ | ||
// the grid | ||
// note that we never remove old keys. | ||
// => over time, HashSet<T>s will be allocated for every possible | ||
// grid position in the world | ||
// => Clear() doesn't clear them so we don't constantly reallocate the | ||
// entries when populating the grid in every Update() call | ||
// => makes the code a lot easier too | ||
// => this is FINE because in the worst case, every grid position in the | ||
// game world is filled with a player anyway! | ||
readonly Dictionary<Vector3Int, HashSet<T>> grid; | ||
|
||
// cache a 9 x 3 neighbor grid of vector3 offsets so we can use them more easily | ||
readonly Vector3Int[] neighbourOffsets; | ||
|
||
public Grid3D(int initialCapacity) | ||
{ | ||
grid = new Dictionary<Vector3Int, HashSet<T>>(initialCapacity); | ||
|
||
neighbourOffsets = new Vector3Int[9 * 3]; | ||
int i = 0; | ||
for (int x = -1; x <= 1; x++) | ||
{ | ||
for (int y = -1; y <= 1; y++) | ||
{ | ||
for (int z = -1; z <= 1; z++) | ||
{ | ||
neighbourOffsets[i] = new Vector3Int(x, y, z); | ||
i += 1; | ||
} | ||
} | ||
} | ||
} | ||
|
||
// helper function so we can add an entry without worrying | ||
public void Add(Vector3Int position, T value) | ||
{ | ||
// initialize set in grid if it's not in there yet | ||
if (!grid.TryGetValue(position, out HashSet<T> hashSet)) | ||
{ | ||
// each grid entry may hold hundreds of entities. | ||
// let's create the HashSet with a large initial capacity | ||
// in order to avoid resizing & allocations. | ||
#if !UNITY_2021_3_OR_NEWER | ||
// Unity 2019 doesn't have "new HashSet(capacity)" yet | ||
hashSet = new HashSet<T>(); | ||
#else | ||
hashSet = new HashSet<T>(128); | ||
#endif | ||
grid[position] = hashSet; | ||
} | ||
|
||
// add to it | ||
hashSet.Add(value); | ||
} | ||
|
||
// helper function to get set at position without worrying | ||
// -> result is passed as parameter to avoid allocations | ||
// -> result is not cleared before. this allows us to pass the HashSet from | ||
// GetWithNeighbours and avoid .UnionWith which is very expensive. | ||
void GetAt(Vector3Int position, HashSet<T> result) | ||
{ | ||
// return the set at position | ||
if (grid.TryGetValue(position, out HashSet<T> hashSet)) | ||
{ | ||
foreach (T entry in hashSet) | ||
result.Add(entry); | ||
} | ||
} | ||
|
||
// helper function to get at position and it's 8 neighbors without worrying | ||
// -> result is passed as parameter to avoid allocations | ||
public void GetWithNeighbours(Vector3Int position, HashSet<T> result) | ||
{ | ||
// clear result first | ||
result.Clear(); | ||
|
||
// add neighbours | ||
foreach (Vector3Int offset in neighbourOffsets) | ||
GetAt(position + offset, result); | ||
} | ||
|
||
// clear: clears the whole grid | ||
// IMPORTANT: we already allocated HashSet<T>s and don't want to do | ||
// reallocate every single update when we rebuild the grid. | ||
// => so simply remove each position's entries, but keep | ||
// every position in there | ||
// => see 'grid' comments above! | ||
// => named ClearNonAlloc to make it more obvious! | ||
public void ClearNonAlloc() | ||
{ | ||
foreach (HashSet<T> hashSet in grid.Values) | ||
hashSet.Clear(); | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
146 changes: 146 additions & 0 deletions
146
...Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// extremely fast spatial hashing interest management based on uMMORPG GridChecker. | ||
// => 30x faster in initial tests | ||
// => scales way higher | ||
// checks on three dimensions (XYZ) which includes the vertical axes. | ||
// this is slower than XY checking for regular spatial hashing. | ||
using System.Collections.Generic; | ||
using UnityEngine; | ||
|
||
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; | ||
|
||
// 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. | ||
// | ||
// 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. | ||
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.")] | ||
public float rebuildInterval = 1; | ||
double lastRebuildTime; | ||
|
||
[Header("Debug Settings")] | ||
public bool showSlider; | ||
|
||
// the grid | ||
// begin with a large capacity to avoid resizing & allocations. | ||
Grid3D<NetworkConnectionToClient> grid = new Grid3D<NetworkConnectionToClient>(1024); | ||
|
||
// project 3d world position to grid position | ||
Vector3Int ProjectToGrid(Vector3 position) => | ||
Vector3Int.RoundToInt(position / resolution); | ||
|
||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) | ||
{ | ||
// calculate projected positions | ||
Vector3Int projected = ProjectToGrid(identity.transform.position); | ||
Vector3Int observerProjected = ProjectToGrid(newObserver.identity.transform.position); | ||
|
||
// distance needs to be at max one of the 8 neighbors, which is | ||
// 1 for the direct neighbors | ||
// 1.41 for the diagonal neighbors (= sqrt(2)) | ||
// => use sqrMagnitude and '2' to avoid computations. same result. | ||
return (projected - observerProjected).sqrMagnitude <= 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance | ||
} | ||
|
||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers) | ||
{ | ||
// add everyone in 9 neighbour grid | ||
// -> pass observers to GetWithNeighbours directly to avoid allocations | ||
// and expensive .UnionWith computations. | ||
Vector3Int current = ProjectToGrid(identity.transform.position); | ||
grid.GetWithNeighbours(current, newObservers); | ||
} | ||
|
||
[ServerCallback] | ||
public override void ResetState() | ||
{ | ||
lastRebuildTime = 0D; | ||
} | ||
|
||
// update everyone's position in the grid | ||
// (internal so we can update from tests) | ||
[ServerCallback] | ||
internal void Update() | ||
{ | ||
// NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL | ||
// entities every INTERVAL. consider the other approach later. | ||
|
||
// IMPORTANT: 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. | ||
// => players do move all the time. we want them to always be in the | ||
// 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. | ||
|
||
// 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 | ||
grid.ClearNonAlloc(); | ||
|
||
// put every connection into the grid at it's main player's position | ||
// NOTE: player sees in a radius around him. NOT around his pet too. | ||
foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values) | ||
{ | ||
// authenticated and joined world with a player? | ||
if (connection.isAuthenticated && connection.identity != null) | ||
{ | ||
// calculate current grid position | ||
Vector3Int position = ProjectToGrid(connection.identity.transform.position); | ||
|
||
// put into grid | ||
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; | ||
} | ||
} | ||
|
||
// OnGUI allocates even if it does nothing. avoid in release. | ||
#if UNITY_EDITOR || DEVELOPMENT_BUILD | ||
// slider from dotsnet. it's nice to play around with in the benchmark | ||
// demo. | ||
void OnGUI() | ||
{ | ||
if (!showSlider) return; | ||
|
||
// only show while server is running. not on client, etc. | ||
if (!NetworkServer.active) return; | ||
|
||
int height = 30; | ||
int width = 250; | ||
GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height)); | ||
GUILayout.BeginHorizontal("Box"); | ||
GUILayout.Label("Radius:"); | ||
visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150))); | ||
GUILayout.Label(visRange.ToString()); | ||
GUILayout.EndHorizontal(); | ||
GUILayout.EndArea(); | ||
} | ||
#endif | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
...r/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
2 changes: 2 additions & 0 deletions
2
...s/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
Assets/Mirror/Tests/Editor/InterestManagement/Grid3DTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
using System.Collections.Generic; | ||
using NUnit.Framework; | ||
using UnityEngine; | ||
|
||
namespace Mirror.Tests.InterestManagement | ||
{ | ||
public class Grid3DTests | ||
{ | ||
Grid3D<int> grid; | ||
|
||
[SetUp] | ||
public void SetUp() | ||
{ | ||
grid = new Grid3D<int>(10); | ||
} | ||
|
||
[Test] | ||
public void AddAndGetNeighbours() | ||
{ | ||
// add two at (0, 0, 0) | ||
grid.Add(Vector3Int.zero, 1); | ||
grid.Add(Vector3Int.zero, 2); | ||
HashSet<int> result = new HashSet<int>(); | ||
grid.GetWithNeighbours(Vector3Int.zero, result); | ||
Assert.That(result.Count, Is.EqualTo(2)); | ||
Assert.That(result.Contains(1), Is.True); | ||
Assert.That(result.Contains(2), Is.True); | ||
|
||
// add a neighbour at (1, 0, 1) | ||
grid.Add(new Vector3Int(1, 0, 1), 3); | ||
grid.GetWithNeighbours(Vector3Int.zero, result); | ||
Assert.That(result.Count, Is.EqualTo(3)); | ||
Assert.That(result.Contains(1), Is.True); | ||
Assert.That(result.Contains(2), Is.True); | ||
Assert.That(result.Contains(3), Is.True); | ||
|
||
// add a neighbour at (1, 1, 1) to test upper layer | ||
grid.Add(new Vector3Int(1, 1, 1), 4); | ||
grid.GetWithNeighbours(Vector3Int.zero, result); | ||
Assert.That(result.Count, Is.EqualTo(4)); | ||
Assert.That(result.Contains(1), Is.True); | ||
Assert.That(result.Contains(2), Is.True); | ||
Assert.That(result.Contains(3), Is.True); | ||
Assert.That(result.Contains(4), Is.True); | ||
|
||
// add a neighbour at (1, -1, 1) to test upper layer | ||
grid.Add(new Vector3Int(1, -1, 1), 5); | ||
grid.GetWithNeighbours(Vector3Int.zero, result); | ||
Assert.That(result.Count, Is.EqualTo(5)); | ||
Assert.That(result.Contains(1), Is.True); | ||
Assert.That(result.Contains(2), Is.True); | ||
Assert.That(result.Contains(3), Is.True); | ||
Assert.That(result.Contains(4), Is.True); | ||
Assert.That(result.Contains(5), Is.True); | ||
} | ||
|
||
[Test] | ||
public void GetIgnoresTooFarNeighbours() | ||
{ | ||
// add at (0, 0, 0) | ||
grid.Add(Vector3Int.zero, 1); | ||
|
||
// get at (2, 0, 0) which is out of 9 neighbour radius | ||
HashSet<int> result = new HashSet<int>(); | ||
grid.GetWithNeighbours(new Vector3Int(2, 0, 0), result); | ||
Assert.That(result.Count, Is.EqualTo(0)); | ||
} | ||
|
||
[Test] | ||
public void ClearNonAlloc() | ||
{ | ||
// add some | ||
grid.Add(Vector3Int.zero, 1); | ||
grid.Add(Vector3Int.zero, 2); | ||
|
||
// clear and check if empty now | ||
grid.ClearNonAlloc(); | ||
HashSet<int> result = new HashSet<int>(); | ||
grid.GetWithNeighbours(Vector3Int.zero, result); | ||
Assert.That(result.Count, Is.EqualTo(0)); | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
Assets/Mirror/Tests/Editor/InterestManagement/Grid3DTests.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.