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: