Skip to content

Commit 561ca26

Browse files
committed
wip faster spatial aoi using imbase
1 parent a7e8b19 commit 561ca26

4 files changed

Lines changed: 342 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Mirror;
4+
using UnityEngine;
5+
6+
public class FastSpatialInterestManagement : InterestManagementBase {
7+
[Tooltip("The maximum range that objects will be visible at.")]
8+
public int visRange = 30;
9+
10+
private int TileSize => visRange / 3;
11+
12+
// the grid
13+
private Dictionary<Vector2Int, HashSet<NetworkIdentity>> grid =
14+
new Dictionary<Vector2Int, HashSet<NetworkIdentity>>();
15+
16+
class Tracked {
17+
public bool uninitialized;
18+
public Vector2Int position;
19+
public Transform transform;
20+
public NetworkIdentity identity;
21+
}
22+
23+
private Dictionary<NetworkIdentity, Tracked> tracked = new Dictionary<NetworkIdentity, Tracked>();
24+
25+
public override void Rebuild(NetworkIdentity identity, bool initialize) {
26+
// do nothing, we update every frame.
27+
}
28+
29+
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) {
30+
// we build initial state during the normal loop too
31+
return false;
32+
}
33+
34+
// update everyone's position in the grid
35+
internal void LateUpdate() {
36+
// only on server
37+
if (!NetworkServer.active) return;
38+
39+
RebuildAll();
40+
}
41+
42+
// When a new entity is spawned
43+
public override void OnSpawned(NetworkIdentity identity) {
44+
// (limitation: we never expect identity.visibile to change)
45+
if (identity.visible != Visibility.Default) {
46+
return;
47+
}
48+
49+
// host visibility shim to make sure unseen entities are hidden
50+
if (NetworkClient.active) {
51+
SetHostVisibility(identity, false);
52+
}
53+
54+
if (identity.connectionToClient != null) {
55+
// client always sees itself
56+
AddObserver(identity.connectionToClient, identity);
57+
}
58+
59+
tracked.Add(identity, new Tracked {
60+
uninitialized = true,
61+
position = new Vector2Int(int.MaxValue, int.MaxValue), // invalid
62+
transform = identity.transform,
63+
identity = identity,
64+
});
65+
}
66+
67+
// when an entity is despawned/destroyed
68+
public override void OnDestroyed(NetworkIdentity identity) {
69+
// (limitation: we never expect identity.visibile to change)
70+
if (identity.visible != Visibility.Default) {
71+
return;
72+
}
73+
74+
var obj = tracked[identity];
75+
tracked.Remove(identity);
76+
77+
if (!obj.uninitialized) {
78+
// observers are cleaned up automatically when destroying, we just need to remove it from our grid
79+
grid[obj.position].Remove(identity);
80+
}
81+
}
82+
83+
private void RebuildAll() {
84+
// loop over all entities and check if their positions changed
85+
foreach (var trackedEntity in tracked.Values) {
86+
Vector2Int pos =
87+
Vector2Int.RoundToInt(
88+
new Vector2(trackedEntity.transform.position.x, trackedEntity.transform.position.z) / TileSize);
89+
if (pos != trackedEntity.position) {
90+
// if the position changed, move entity about
91+
Vector2Int oldPos = trackedEntity.position;
92+
trackedEntity.position = pos;
93+
// First: Remove from old grid position, but only if it was ever in the grid
94+
if (!trackedEntity.uninitialized) {
95+
RebuildRemove(trackedEntity.identity, oldPos, pos);
96+
}
97+
98+
RebuildAdd(trackedEntity.identity, oldPos, pos, trackedEntity.uninitialized);
99+
trackedEntity.uninitialized = false;
100+
}
101+
}
102+
}
103+
104+
private void RebuildRemove(NetworkIdentity entity, Vector2Int oldPosition, Vector2Int newPosition) {
105+
// sanity check
106+
if (!grid[oldPosition].Remove(entity)) {
107+
throw new InvalidOperationException("entity was not in the provided grid");
108+
}
109+
110+
// for all tiles the entity could see at the old position
111+
for (int x = -1; x <= 1; x++) {
112+
for (int y = -1; y <= 1; y++) {
113+
var tilePos = oldPosition + new Vector2Int(x, y);
114+
// optimization: don't remove on overlapping tiles
115+
if (Mathf.Abs(tilePos.x - newPosition.x) <= 1 &&
116+
Mathf.Abs(tilePos.y - newPosition.y) <= 1) {
117+
continue;
118+
}
119+
120+
if (!grid.TryGetValue(tilePos, out HashSet<NetworkIdentity> tile)) {
121+
continue;
122+
}
123+
124+
// update observers for all identites the entity could see and all players that could see it
125+
foreach (NetworkIdentity identity in tile) {
126+
// dont touch yourself (hah.)
127+
if (identity == entity) {
128+
continue;
129+
}
130+
131+
// if the identity is a player, remove the entity from it
132+
if (identity.connectionToClient != null) {
133+
RemoveObserver(identity.connectionToClient, entity);
134+
}
135+
136+
// if the entity is a player, remove the identity from it
137+
if (entity.connectionToClient != null) {
138+
RemoveObserver(entity.connectionToClient, identity);
139+
}
140+
}
141+
}
142+
}
143+
}
144+
145+
private void RebuildAdd(NetworkIdentity entity, Vector2Int oldPos, Vector2Int newPos, bool initialize) {
146+
// for all tiles the entity now sees at the new position
147+
for (int x = -1; x <= 1; x++) {
148+
for (int y = -1; y <= 1; y++) {
149+
var tilePos = newPos + new Vector2Int(x, y);
150+
// optimization: don't add on overlapping tiles
151+
if (!initialize && (Mathf.Abs(tilePos.x - oldPos.x) <= 1 &&
152+
Mathf.Abs(tilePos.y - oldPos.y) <= 1)) {
153+
continue;
154+
}
155+
156+
if (!grid.TryGetValue(tilePos, out var tile)) {
157+
continue;
158+
}
159+
160+
foreach (var identity in tile) {
161+
// dont touch yourself (hah.)
162+
if (identity == entity) {
163+
continue;
164+
}
165+
166+
// if the identity is a player, add the entity to it
167+
if (identity.connectionToClient != null) {
168+
try {
169+
AddObserver(identity.connectionToClient, entity);
170+
} catch (ArgumentException e) {
171+
// sanity check
172+
Debug.LogError(
173+
$"Failed to add {entity} (#{entity.netId}) to the observers of {identity} (#{identity.netId}) (case 1)\n{e}");
174+
}
175+
}
176+
177+
// if the entity is a player, add the identity to it
178+
if (entity.connectionToClient != null) {
179+
try {
180+
AddObserver(entity.connectionToClient, identity);
181+
} catch (ArgumentException e) {
182+
// sanity check
183+
Debug.LogError(
184+
$"Failed to add {identity} (#{identity.netId}) to the observers of {entity} (#{entity.netId}) (case 2)\n{e}");
185+
}
186+
}
187+
}
188+
}
189+
}
190+
191+
// add ourselves to the new grid position
192+
if (!grid.TryGetValue(newPos, out HashSet<NetworkIdentity> addTile)) {
193+
addTile = new HashSet<NetworkIdentity>();
194+
grid[newPos] = addTile;
195+
}
196+
197+
if (!addTile.Add(entity)) {
198+
throw new InvalidOperationException("entity was already in the grid");
199+
}
200+
}
201+
}

Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// default = no component = everyone sees everyone
2+
3+
using System.Collections.Generic;
4+
using NUnit.Framework;
5+
using UnityEngine;
6+
7+
namespace Mirror.Tests
8+
{
9+
public class InterestManagementTests_FastSpatialHashing : InterestManagementTests_Common
10+
{
11+
FastSpatialInterestManagement aoi;
12+
13+
[SetUp]
14+
public override void SetUp()
15+
{
16+
17+
// TODO: these are just copied from the base Setup methods since the aoi expects "normal" operation
18+
// for example: OnSpawned to be called for spawning identities, late adding the aoi does not work currently
19+
// the setup also adds each identity to spawned twice so that also causes some issues during teardown
20+
instantiated = new List<GameObject>();
21+
22+
// need a holder GO. with name for easier debugging.
23+
holder = new GameObject("MirrorTest.holder");
24+
25+
// need a transport to send & receive
26+
Transport.active = transport = holder.AddComponent<MemoryTransport>();
27+
28+
// A with connectionId = 0x0A, netId = 0xAA
29+
CreateNetworked(out gameObjectA, out identityA);
30+
connectionA = new NetworkConnectionToClient(0x0A);
31+
connectionA.isAuthenticated = true;
32+
connectionA.isReady = true;
33+
connectionA.identity = identityA;
34+
//NetworkServer.spawned[0xAA] = identityA; // TODO: this causes two the identities to end up in spawned twice
35+
36+
// B
37+
CreateNetworked(out gameObjectB, out identityB);
38+
connectionB = new NetworkConnectionToClient(0x0B);
39+
connectionB.isAuthenticated = true;
40+
connectionB.isReady = true;
41+
connectionB.identity = identityB;
42+
//NetworkServer.spawned[0xBB] = identityB; // TODO: this causes two the identities to end up in spawned twice
43+
44+
// need to start server so that interest management works
45+
NetworkServer.Listen(10);
46+
47+
// add both connections
48+
NetworkServer.connections[connectionA.connectionId] = connectionA;
49+
NetworkServer.connections[connectionB.connectionId] = connectionB;
50+
51+
aoi = holder.AddComponent<FastSpatialInterestManagement>();
52+
aoi.visRange = 10;
53+
// setup server aoi since InterestManagement Awake isn't called
54+
NetworkServer.aoi = aoi;
55+
56+
// spawn both so that .observers is created
57+
NetworkServer.Spawn(gameObjectA, connectionA);
58+
NetworkServer.Spawn(gameObjectB, connectionB);
59+
}
60+
61+
[TearDown]
62+
public override void TearDown()
63+
{
64+
base.TearDown();
65+
// clear server aoi again
66+
NetworkServer.aoi = null;
67+
}
68+
69+
public override void ForceHidden_Initial()
70+
{
71+
// doesnt support changing visibility at runtime
72+
}
73+
74+
public override void ForceShown_Initial()
75+
{
76+
// doesnt support changing visibility at runtime
77+
}
78+
79+
// brute force interest management
80+
// => everyone should see everyone if in range
81+
[Test]
82+
public void InRange_Initial()
83+
{
84+
// A and B are at (0,0,0) so within range!
85+
86+
aoi.LateUpdate();
87+
// both should see each other because they are in range
88+
Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.True);
89+
Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.True);
90+
}
91+
92+
// brute force interest management
93+
// => everyone should see everyone if in range
94+
[Test]
95+
public void InRange_NotInitial()
96+
{
97+
// A and B are at (0,0,0) so within range!
98+
99+
aoi.LateUpdate();
100+
// both should see each other because they are in range
101+
Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.True);
102+
Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.True);
103+
}
104+
105+
// brute force interest management
106+
// => everyone should see everyone if in range
107+
[Test]
108+
public void OutOfRange_Initial()
109+
{
110+
// A and B are too far from each other
111+
identityB.transform.position = Vector3.right * (aoi.visRange + 1);
112+
113+
aoi.LateUpdate();
114+
// both should not see each other
115+
Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.False);
116+
Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.False);
117+
}
118+
119+
// brute force interest management
120+
// => everyone should see everyone if in range
121+
[Test]
122+
public void OutOfRange_NotInitial()
123+
{
124+
// A and B are too far from each other
125+
identityB.transform.position = Vector3.right * (aoi.visRange + 1);
126+
127+
aoi.LateUpdate();
128+
// both should not see each other
129+
Assert.That(identityA.observers.ContainsKey(connectionB.connectionId), Is.False);
130+
Assert.That(identityB.observers.ContainsKey(connectionA.connectionId), Is.False);
131+
}
132+
133+
// TODO add tests to make sure old observers are removed etc.
134+
}
135+
}

Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)