mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 19:10:32 +00:00
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>
This commit is contained in:
parent
de870f3d67
commit
849a293b94
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b157c08313c64752b0856469b1b70771
|
||||
timeCreated: 1713533175
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 120b4d6121d94e0280cd2ec536b0ea8f
|
||||
timeCreated: 1713534045
|
@ -1,6 +1,8 @@
|
||||
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
|
||||
// => 30x faster in initial tests
|
||||
// => scales way higher
|
||||
// checks on two dimensions only(!), for example: XZ for 3D games or XY for 2D games.
|
||||
// this is faster than XYZ checking but doesn't check vertical distance.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
|
83
Assets/Mirror/Tests/Editor/InterestManagement/Grid3DTests.cs
Normal file
83
Assets/Mirror/Tests/Editor/InterestManagement/Grid3DTests.cs
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7aeeeb0f063e4f1c97fdde55fa0a4179
|
||||
timeCreated: 1713533447
|
Loading…
Reference in New Issue
Block a user