From 3d46d9f511ce05819507059fae1717d533dfb4b9 Mon Sep 17 00:00:00 2001 From: James Frowen Date: Mon, 1 Nov 2021 19:52:51 +0000 Subject: [PATCH] perf(Visibility): adding SpatialHash components --- .../Components/Visibility/SpatialHash.meta | 8 + .../SpatialHash/SpatialHashSystem.cs | 238 ++++++++++++++++++ .../SpatialHash/SpatialHashSystem.cs.meta | 11 + .../SpatialHash/SpatialHashVisibility.cs | 41 +++ .../SpatialHash/SpatialHashVisibility.cs.meta | 11 + 5 files changed, 309 insertions(+) create mode 100644 Assets/Mirage/Components/Visibility/SpatialHash.meta create mode 100644 Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs create mode 100644 Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs.meta create mode 100644 Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs create mode 100644 Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs.meta diff --git a/Assets/Mirage/Components/Visibility/SpatialHash.meta b/Assets/Mirage/Components/Visibility/SpatialHash.meta new file mode 100644 index 00000000000..532fbc8a0b9 --- /dev/null +++ b/Assets/Mirage/Components/Visibility/SpatialHash.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bfeca0b13b830434a890072ea27e57ec +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs new file mode 100644 index 00000000000..0dcd2f63391 --- /dev/null +++ b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs @@ -0,0 +1,238 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirage.Visibility.SpatialHash +{ + internal static class SpatialHashExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector2 ToXZ(this Vector3 v) => new Vector2(v.x, v.z); + } + + public class SpatialHashSystem : MonoBehaviour + { + public NetworkServer Server; + + /// + /// How often (in seconds) that this object should update the list of observers that can see it. + /// + [Tooltip("How often (in seconds) that this object should update the list of observers that can see it.")] + public float VisibilityUpdateInterval = 1; + + [Tooltip("height and width of 1 box in grid")] + public float gridSize = 10; + + public Vector2 Centre = new Vector2(0, 0); + + [Tooltip("Bounds of the map used to calculate visibility. Objects out side of grid will not be visibility")] + public Vector2 Size = new Vector2(100, 100); + + // todo is list vs hashset better? Set would be better for remove objects, list would be better for looping + List all = new List(); + public GridHolder Grid; + + + public void Awake() + { + Server.Started.AddListener(() => + { + Server.World.onSpawn += World_onSpawn; + Server.World.onUnspawn += World_onUnspawn; + + // skip first invoke, list will be empty + InvokeRepeating(nameof(RebuildObservers), VisibilityUpdateInterval, VisibilityUpdateInterval); + + Grid = new GridHolder(gridSize, Centre, Size); + }); + + Server.Stopped.AddListener(() => + { + CancelInvoke(nameof(RebuildObservers)); + Grid = null; + }); + } + + private void World_onSpawn(NetworkIdentity identity) + { + NetworkVisibility visibility = identity.Visibility; + if (visibility is SpatialHashVisibility obj) + { + obj.System = this; + all.Add(obj); + } + } + private void World_onUnspawn(NetworkIdentity identity) + { + NetworkVisibility visibility = identity.Visibility; + if (visibility is SpatialHashVisibility obj) + { + all.Remove(obj); + } + } + + void RebuildObservers() + { + ClearGrid(); + AddPlayersToGrid(); + + foreach (SpatialHashVisibility obj in all) + { + obj.Identity.RebuildObservers(false); + } + } + + private void ClearGrid() + { + for (int i = 0; i < Grid.Width; i++) + { + for (int j = 0; j < Grid.Width; j++) + { + HashSet set = Grid.GetObjects(i, j); + if (set != null) + { + set.Clear(); + } + } + } + } + + private void AddPlayersToGrid() + { + foreach (INetworkPlayer player in Server.Players) + { + if (!player.HasCharacter) + continue; + + Vector2 position = player.Identity.transform.position.ToXZ(); + Grid.AddObject(position, player); + } + } + + + public class GridHolder + { + public readonly int Width; + public readonly int Height; + public readonly float GridSize; + public readonly Vector2 Centre; + public readonly Vector2 Size; + + public readonly GridPoint[] Points; + + public GridHolder(float gridSize, Vector2 centre, Vector2 size) + { + Centre = centre; + Size = size; + Width = Mathf.CeilToInt(size.x / gridSize); + Height = Mathf.CeilToInt(size.y / gridSize); + GridSize = gridSize; + + Points = new GridPoint[Width * Height]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddObject(Vector2 position, T obj) + { + ToGridIndex(position, out int x, out int y); + AddObject(x, y, obj); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddObject(int i, int j, T obj) + { + int index = i + j * Width; + if (Points[index].objects == null) + { + Points[index].objects = new HashSet(); + } + + Points[index].objects.Add(obj); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashSet GetObjects(int i, int j) + { + return Points[i + j * Width].objects; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CreateSet(int i, int j) + { + Points[i + j * Width].objects = new HashSet(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool InBounds(Vector2 position) + { + float x = position.x - Centre.x; + float y = position.y - Centre.y; + + return (0 < x && x < Size.x) + && (0 < y && y < Size.y); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool InBounds(int x, int y) + { + return (0 < x && x < Width) + && (0 < y && y < Height); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsVisible(Vector2 target, Vector2 player, int range) + { + // if either is out of bounds, not visible + if (!InBounds(target) || !InBounds(player)) return false; + + ToGridIndex(target, out int xt, out int yt); + ToGridIndex(player, out int xp, out int yp); + + return AreClose(xt, xp, range) && AreClose(yt, yp, range); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool AreClose(int a, int b, int range) + { + int min = a - range; + int max = a + range; + + return max <= b && b <= min; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToGridIndex(Vector2 position, out int x, out int y) + { + float fx = position.x - Centre.x; + float fy = position.y - Centre.y; + + x = Mathf.RoundToInt(fx / GridSize); + y = Mathf.RoundToInt(fy / GridSize); + } + + public void BuildObservers(HashSet observers, Vector2 position, int range) + { + // not visible if not in range + if (!InBounds(position)) + return; + + ToGridIndex(position - Centre, out int x, out int y); + + for (int i = x - range; i <= x + range; i++) + { + for (int j = y - range; j <= y + range; j++) + { + if (InBounds(i, j)) + { + observers.UnionWith(GetObjects(i, j)); + } + } + } + } + + public struct GridPoint + { + public HashSet objects; + } + } + } +} diff --git a/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs.meta b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs.meta new file mode 100644 index 00000000000..572f712a843 --- /dev/null +++ b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 92208a774ad18fd4ab07187078fc495d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs new file mode 100644 index 00000000000..4ee0cd466bd --- /dev/null +++ b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Mirage.Logging; +using UnityEngine; + +namespace Mirage.Visibility.SpatialHash +{ + public class SpatialHashVisibility : NetworkVisibility + { + static readonly ILogger logger = LogFactory.GetLogger(typeof(SpatialHashVisibility)); + + [Tooltip("How many grid away the player can be to see this object. Real distance is this mutlipled by SpatialHashSystem")] + public int GridVisibleRange = 1; + + public SpatialHashSystem System; + + /// Network connection of a player. + /// True if the player can see this object. + public override bool OnCheckObserver(INetworkPlayer player) + { + if (player.Identity == null) + return false; + + + Vector2 thisPosition = transform.position.ToXZ(); + Vector2 playerPosition = player.Identity.transform.position.ToXZ(); + + return System.Grid.IsVisible(thisPosition, playerPosition, GridVisibleRange); + } + + /// + /// Callback used by the visibility system to (re)construct the set of observers that can see this object. + /// Implementations of this callback should add network connections of players that can see this object to the observers set. + /// + /// The new set of observers for this object. + /// True if the set of observers is being built for the first time. + public override void OnRebuildObservers(HashSet observers, bool initialize) + { + System.Grid.BuildObservers(observers, transform.position.ToXZ(), GridVisibleRange); + } + } +} diff --git a/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs.meta b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs.meta new file mode 100644 index 00000000000..25a2b2f9976 --- /dev/null +++ b/Assets/Mirage/Components/Visibility/SpatialHash/SpatialHashVisibility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d10af5e22d6a3f449974d5c78f5eb4df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: