wip faster spatial aoi using imbase

This commit is contained in:
Robin Rolf 2023-02-11 16:04:04 +00:00
parent dd7337c84f
commit 7e1156a4d6
4 changed files with 342 additions and 0 deletions

View File

@ -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");
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 86996600b40a4c41bb300ac0aed33189
timeCreated: 1676129828

View File

@ -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.
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d74fcd64861c40f8856d3479137f49f2
timeCreated: 1676129959