From 7e1156a4d6a0ebf70a42fd95499f153709ffccfb Mon Sep 17 00:00:00 2001 From: Robin Rolf Date: Sat, 11 Feb 2023 16:04:04 +0000 Subject: [PATCH] wip faster spatial aoi using imbase --- .../FastSpatialInterestManagement.cs | 201 ++++++++++++++++++ .../FastSpatialInterestManagement.cs.meta | 3 + ...erestManagementTests_FastSpatialHashing.cs | 135 ++++++++++++ ...ManagementTests_FastSpatialHashing.cs.meta | 3 + 4 files changed, 342 insertions(+) create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs.meta create mode 100644 Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs create mode 100644 Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs.meta diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs new file mode 100644 index 000000000..1bb039cc1 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using Mirror; +using UnityEngine; + +public class FastSpatialInterestManagement : InterestManagementBase { + [Tooltip("The maximum range that objects will be visible at.")] + public int visRange = 30; + + private int TileSize => visRange / 3; + + // the grid + private Dictionary> grid = + new Dictionary>(); + + class Tracked { + public bool uninitialized; + public Vector2Int position; + public Transform transform; + public NetworkIdentity identity; + } + + private Dictionary tracked = new Dictionary(); + + public override void Rebuild(NetworkIdentity identity, bool initialize) { + // do nothing, we update every frame. + } + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { + // we build initial state during the normal loop too + return false; + } + + // update everyone's position in the grid + internal void LateUpdate() { + // only on server + if (!NetworkServer.active) return; + + RebuildAll(); + } + + // When a new entity is spawned + public override void OnSpawned(NetworkIdentity identity) { + // (limitation: we never expect identity.visibile to change) + if (identity.visible != Visibility.Default) { + return; + } + + // host visibility shim to make sure unseen entities are hidden + if (NetworkClient.active) { + SetHostVisibility(identity, false); + } + + if (identity.connectionToClient != null) { + // client always sees itself + AddObserver(identity.connectionToClient, identity); + } + + tracked.Add(identity, new Tracked { + uninitialized = true, + position = new Vector2Int(int.MaxValue, int.MaxValue), // invalid + transform = identity.transform, + identity = identity, + }); + } + + // when an entity is despawned/destroyed + public override void OnDestroyed(NetworkIdentity identity) { + // (limitation: we never expect identity.visibile to change) + if (identity.visible != Visibility.Default) { + return; + } + + var obj = tracked[identity]; + tracked.Remove(identity); + + if (!obj.uninitialized) { + // observers are cleaned up automatically when destroying, we just need to remove it from our grid + grid[obj.position].Remove(identity); + } + } + + private void RebuildAll() { + // loop over all entities and check if their positions changed + foreach (var trackedEntity in tracked.Values) { + Vector2Int pos = + Vector2Int.RoundToInt( + new Vector2(trackedEntity.transform.position.x, trackedEntity.transform.position.z) / TileSize); + if (pos != trackedEntity.position) { + // if the position changed, move entity about + Vector2Int oldPos = trackedEntity.position; + trackedEntity.position = pos; + // First: Remove from old grid position, but only if it was ever in the grid + if (!trackedEntity.uninitialized) { + RebuildRemove(trackedEntity.identity, oldPos, pos); + } + + RebuildAdd(trackedEntity.identity, oldPos, pos, trackedEntity.uninitialized); + trackedEntity.uninitialized = false; + } + } + } + + private void RebuildRemove(NetworkIdentity entity, Vector2Int oldPosition, Vector2Int newPosition) { + // sanity check + if (!grid[oldPosition].Remove(entity)) { + throw new InvalidOperationException("entity was not in the provided grid"); + } + + // for all tiles the entity could see at the old position + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + var tilePos = oldPosition + new Vector2Int(x, y); + // optimization: don't remove on overlapping tiles + if (Mathf.Abs(tilePos.x - newPosition.x) <= 1 && + Mathf.Abs(tilePos.y - newPosition.y) <= 1) { + continue; + } + + if (!grid.TryGetValue(tilePos, out HashSet tile)) { + continue; + } + + // update observers for all identites the entity could see and all players that could see it + foreach (NetworkIdentity identity in tile) { + // dont touch yourself (hah.) + if (identity == entity) { + continue; + } + + // if the identity is a player, remove the entity from it + if (identity.connectionToClient != null) { + RemoveObserver(identity.connectionToClient, entity); + } + + // if the entity is a player, remove the identity from it + if (entity.connectionToClient != null) { + RemoveObserver(entity.connectionToClient, identity); + } + } + } + } + } + + private void RebuildAdd(NetworkIdentity entity, Vector2Int oldPos, Vector2Int newPos, bool initialize) { + // for all tiles the entity now sees at the new position + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + var tilePos = newPos + new Vector2Int(x, y); + // optimization: don't add on overlapping tiles + if (!initialize && (Mathf.Abs(tilePos.x - oldPos.x) <= 1 && + Mathf.Abs(tilePos.y - oldPos.y) <= 1)) { + continue; + } + + if (!grid.TryGetValue(tilePos, out var tile)) { + continue; + } + + foreach (var identity in tile) { + // dont touch yourself (hah.) + if (identity == entity) { + continue; + } + + // if the identity is a player, add the entity to it + if (identity.connectionToClient != null) { + try { + AddObserver(identity.connectionToClient, entity); + } catch (ArgumentException e) { + // sanity check + Debug.LogError( + $"Failed to add {entity} (#{entity.netId}) to the observers of {identity} (#{identity.netId}) (case 1)\n{e}"); + } + } + + // if the entity is a player, add the identity to it + if (entity.connectionToClient != null) { + try { + AddObserver(entity.connectionToClient, identity); + } catch (ArgumentException e) { + // sanity check + Debug.LogError( + $"Failed to add {identity} (#{identity.netId}) to the observers of {entity} (#{entity.netId}) (case 2)\n{e}"); + } + } + } + } + } + + // add ourselves to the new grid position + if (!grid.TryGetValue(newPos, out HashSet addTile)) { + addTile = new HashSet(); + grid[newPos] = addTile; + } + + if (!addTile.Add(entity)) { + throw new InvalidOperationException("entity was already in the grid"); + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs.meta new file mode 100644 index 000000000..c6715b4aa --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 86996600b40a4c41bb300ac0aed33189 +timeCreated: 1676129828 \ No newline at end of file diff --git a/Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs b/Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs new file mode 100644 index 000000000..d89dea5f1 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs @@ -0,0 +1,135 @@ +// default = no component = everyone sees everyone + +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; + +namespace Mirror.Tests +{ + public class InterestManagementTests_FastSpatialHashing : InterestManagementTests_Common + { + FastSpatialInterestManagement aoi; + + [SetUp] + public override void SetUp() + { + + // TODO: these are just copied from the base Setup methods since the aoi expects "normal" operation + // for example: OnSpawned to be called for spawning identities, late adding the aoi does not work currently + // the setup also adds each identity to spawned twice so that also causes some issues during teardown + instantiated = new List(); + + // need a holder GO. with name for easier debugging. + holder = new GameObject("MirrorTest.holder"); + + // need a transport to send & receive + Transport.active = transport = holder.AddComponent(); + + // A with connectionId = 0x0A, netId = 0xAA + CreateNetworked(out gameObjectA, out identityA); + connectionA = new NetworkConnectionToClient(0x0A); + connectionA.isAuthenticated = true; + connectionA.isReady = true; + connectionA.identity = identityA; + //NetworkServer.spawned[0xAA] = identityA; // TODO: this causes two the identities to end up in spawned twice + + // B + CreateNetworked(out gameObjectB, out identityB); + connectionB = new NetworkConnectionToClient(0x0B); + connectionB.isAuthenticated = true; + connectionB.isReady = true; + connectionB.identity = identityB; + //NetworkServer.spawned[0xBB] = identityB; // TODO: this causes two the identities to end up in spawned twice + + // need to start server so that interest management works + NetworkServer.Listen(10); + + // add both connections + NetworkServer.connections[connectionA.connectionId] = connectionA; + NetworkServer.connections[connectionB.connectionId] = connectionB; + + aoi = holder.AddComponent(); + aoi.visRange = 10; + // setup server aoi since InterestManagement Awake isn't called + NetworkServer.aoi = aoi; + + // spawn both so that .observers is created + NetworkServer.Spawn(gameObjectA, connectionA); + NetworkServer.Spawn(gameObjectB, connectionB); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + // clear server aoi again + NetworkServer.aoi = null; + } + + public override void ForceHidden_Initial() + { + // doesnt support changing visibility at runtime + } + + public override void ForceShown_Initial() + { + // doesnt support changing visibility at runtime + } + + // brute force interest management + // => everyone should see everyone if in range + [Test] + public void InRange_Initial() + { + // A and B are at (0,0,0) so within range! + + aoi.LateUpdate(); + // both should see each other because they are in range + Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.True); + Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.True); + } + + // brute force interest management + // => everyone should see everyone if in range + [Test] + public void InRange_NotInitial() + { + // A and B are at (0,0,0) so within range! + + aoi.LateUpdate(); + // both should see each other because they are in range + Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.True); + Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.True); + } + + // brute force interest management + // => everyone should see everyone if in range + [Test] + public void OutOfRange_Initial() + { + // A and B are too far from each other + identityB.transform.position = Vector3.right * (aoi.visRange + 1); + + aoi.LateUpdate(); + // both should not see each other + Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.False); + Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.False); + } + + // brute force interest management + // => everyone should see everyone if in range + [Test] + public void OutOfRange_NotInitial() + { + // A and B are too far from each other + identityB.transform.position = Vector3.right * (aoi.visRange + 1); + + aoi.LateUpdate(); + // both should not see each other + Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.False); + Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.False); + } + + // TODO add tests to make sure old observers are removed etc. + } +} diff --git a/Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs.meta b/Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs.meta new file mode 100644 index 000000000..94d018b9e --- /dev/null +++ b/Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d74fcd64861c40f8856d3479137f49f2 +timeCreated: 1676129959 \ No newline at end of file