mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
wip faster spatial aoi using imbase
This commit is contained in:
parent
dd7337c84f
commit
7e1156a4d6
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 86996600b40a4c41bb300ac0aed33189
|
||||||
|
timeCreated: 1676129828
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d74fcd64861c40f8856d3479137f49f2
|
||||||
|
timeCreated: 1676129959
|
Loading…
Reference in New Issue
Block a user