-
-
Notifications
You must be signed in to change notification settings - Fork 769
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
342 additions
and
0 deletions.
There are no files selected for viewing
201 changes: 201 additions & 0 deletions
201
Assets/Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.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,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<Vector2Int, HashSet<NetworkIdentity>> grid = | ||
new Dictionary<Vector2Int, HashSet<NetworkIdentity>>(); | ||
|
||
class Tracked { | ||
public bool uninitialized; | ||
public Vector2Int position; | ||
public Transform transform; | ||
public NetworkIdentity identity; | ||
} | ||
|
||
private Dictionary<NetworkIdentity, Tracked> tracked = new Dictionary<NetworkIdentity, Tracked>(); | ||
|
||
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<NetworkIdentity> 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<NetworkIdentity> addTile)) { | ||
addTile = new HashSet<NetworkIdentity>(); | ||
grid[newPos] = addTile; | ||
} | ||
|
||
if (!addTile.Add(entity)) { | ||
throw new InvalidOperationException("entity was already in the grid"); | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
...Mirror/Components/InterestManagement/SpatialHashing/FastSpatialInterestManagement.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
135 changes: 135 additions & 0 deletions
135
Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.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,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<GameObject>(); | ||
|
||
// 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<MemoryTransport>(); | ||
|
||
// 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<FastSpatialInterestManagement>(); | ||
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. | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
Assets/Mirror/Tests/Editor/InterestManagementTests_FastSpatialHashing.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.