Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(Visibility): adding SpatialHash components #982

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
improving spatialhash
James-Frowen committed Apr 30, 2022
commit 44270d6c880a9a000811c21c83cedb315ecffd00
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
@@ -8,6 +9,8 @@ internal static class SpatialHashExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector2 ToXZ(this Vector3 v) => new Vector2(v.x, v.z);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector3 FromXZ(this Vector2 v) => new Vector3(v.x, 0, v.y);
}

public class SpatialHashSystem : MonoBehaviour
@@ -20,19 +23,16 @@ public class SpatialHashSystem : MonoBehaviour
[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);
public Bounds Bounds = new Bounds(Vector3.zero, 100 * Vector3.one);

[Tooltip("How many points to split the grid into (in each xz axis)")]
public Vector2Int GridCount = Vector2Int.one * 10;

// todo is list vs hashset better? Set would be better for remove objects, list would be better for looping
List<SpatialHashVisibility> all = new List<SpatialHashVisibility>();
public GridHolder<INetworkPlayer> Grid;


public void Awake()
{
Server.Started.AddListener(() =>
@@ -43,7 +43,7 @@ public void Awake()
// skip first invoke, list will be empty
InvokeRepeating(nameof(RebuildObservers), VisibilityUpdateInterval, VisibilityUpdateInterval);

Grid = new GridHolder<INetworkPlayer>(gridSize, Centre, Size);
Grid = new GridHolder<INetworkPlayer>(Bounds, GridCount);
});

Server.Stopped.AddListener(() =>
@@ -58,6 +58,7 @@ private void World_onSpawn(NetworkIdentity identity)
NetworkVisibility visibility = identity.Visibility;
if (visibility is SpatialHashVisibility obj)
{
Debug.Assert(obj.System == null);
obj.System = this;
all.Add(obj);
}
@@ -67,6 +68,8 @@ private void World_onUnspawn(NetworkIdentity identity)
NetworkVisibility visibility = identity.Visibility;
if (visibility is SpatialHashVisibility obj)
{
Debug.Assert(obj.System == this);
obj.System = null;
all.Remove(obj);
}
}
@@ -114,28 +117,35 @@ public class GridHolder<T>
{
public readonly int Width;
public readonly int Height;
public readonly float GridSize;
public readonly Vector2 Centre;
public readonly Vector2 Offset;
public readonly Vector2 Size;
public readonly Vector2 Extents;
public readonly Vector2 GridSize;

public readonly GridPoint[] Points;

public GridHolder(float gridSize, Vector2 centre, Vector2 size)
public GridHolder(Bounds bounds, Vector2Int gridCount)
{
Centre = centre;
Size = size;
Width = Mathf.CeilToInt(size.x / gridSize);
Height = Mathf.CeilToInt(size.y / gridSize);
GridSize = gridSize;
Offset = (bounds.center - bounds.extents).ToXZ();
Size = bounds.size.ToXZ();
Extents = bounds.extents.ToXZ();

Width = gridCount.x;
Height = gridCount.y;

GridSize = Size / gridCount;

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);
if (InBounds(position))
{
ToGridIndex(position, out int x, out int y);
AddObject(x, y, obj);
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -153,6 +163,12 @@ public void AddObject(int i, int j, T obj)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public HashSet<T> GetObjects(int i, int j)
{
#if DEBUG
if (i < 0) throw new IndexOutOfRangeException($"i ({i}) is less than zero");
if (j < 0) throw new IndexOutOfRangeException($"j ({j}) is less than zero");
if (i >= Width) throw new IndexOutOfRangeException($"i ({i}) is greater than {Width}");
if (j >= Height) throw new IndexOutOfRangeException($"j ({j}) is greater than {Height}");
#endif
return Points[i + j * Width].objects;
}

@@ -165,17 +181,15 @@ public void CreateSet(int i, int j)
[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);
return (-Extents.x <= position.x && position.x <= Extents.x)
&& (-Extents.y <= position.y && position.y <= Extents.y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool InBounds(int x, int y)
{
return (0 < x && x < Width)
&& (0 < y && y < Height);
// inclusive lower bound
return (0 <= x && x < Width)
&& (0 <= y && y < Height);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -202,11 +216,11 @@ bool AreClose(int a, int b, int range)
[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;
float fx = position.x - Offset.x;
float fy = position.y - Offset.y;

x = Mathf.RoundToInt(fx / GridSize);
y = Mathf.RoundToInt(fy / GridSize);
x = Mathf.RoundToInt(fx / GridSize.x);
y = Mathf.RoundToInt(fy / GridSize.y);
}

public void BuildObservers(HashSet<T> observers, Vector2 position, int range)
@@ -215,20 +229,32 @@ public void BuildObservers(HashSet<T> observers, Vector2 position, int range)
if (!InBounds(position))
return;

ToGridIndex(position - Centre, out int x, out int y);
ToGridIndex(position, 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));
HashSet<T> set = GetObjects(i, j);
if (set != null)
UnionWithNonAlloc(observers, set);
}
}
}
}

void UnionWithNonAlloc(HashSet<T> first, HashSet<T> second)
{
HashSet<T>.Enumerator enumerator = second.GetEnumerator();
while (enumerator.MoveNext())
{
first.Add(enumerator.Current);
}
enumerator.Dispose();
}

public struct GridPoint
{
public HashSet<T> objects;
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using UnityEngine;

#if UNITY_EDITOR
namespace Mirage.Visibility.SpatialHash.EditorScripts
{
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;

[CustomEditor(typeof(SpatialHashSystem))]
public class SpatialHashSystemEditor : Editor
{
const PrimitiveBoundsHandle.Axes BoundsAxis = PrimitiveBoundsHandle.Axes.X | PrimitiveBoundsHandle.Axes.Z;

BoxBoundsHandle boundsHandle = new BoxBoundsHandle() { axes = BoundsAxis };

// cache array for drawing handle
readonly Vector3[] verts = new Vector3[4];

readonly List<Vector3> playerPositions = new List<Vector3>();

public void OnSceneGUI()
{
var system = target as SpatialHashSystem;
var bounds = new Rect(system.Bounds.center.ToXZ(), system.Bounds.size.ToXZ());
Vector2Int points = system.GridCount;
GetPlayerPos(system);

DrawGrid(bounds, points, playerPositions);
DrawBoundsHandle(system);
}

private void GetPlayerPos(SpatialHashSystem system)
{
playerPositions.Clear();

NetworkServer server = system.Server;
if (server != null && server.Active)
{
foreach (INetworkPlayer player in server.Players)
{
if (player.HasCharacter)
{
NetworkIdentity character = player.Identity;
playerPositions.Add(character.transform.position);
}
}

// return here, dont need to check client if we are server
return;
}

NetworkClient client = system.GetComponent<NetworkClient>();
if (client != null && client.Active)
{
if (client.Player != null && client.Player.HasCharacter)
{
NetworkIdentity character = client.Player.Identity;
playerPositions.Add(character.transform.position);
return;

}
}
}

private void DrawGrid(Rect bounds, Vector2Int points, List<Vector3> players)
{
Vector2 offset = bounds.center - bounds.size;
Vector2 size = bounds.size;

var gridSize = new Vector2(size.x / points.x, size.y / points.y);
Vector2 halfGrid = gridSize / 2;

bool colorFlip = false;
for (int i = 0; i < points.x; i++)
{
for (int j = 0; j < points.y; j++)
{
float x = i * gridSize.x;
float y = j * gridSize.y;

// center of 1 gridPoint
Vector2 pos2d = new Vector2(x, y) + halfGrid + offset;
Vector3 pos = pos2d.FromXZ();

// +- halfGrid to get corners
verts[0] = pos + new Vector3(-halfGrid.x, 0, -halfGrid.y);
verts[1] = pos + new Vector3(-halfGrid.x, 0, +halfGrid.y);
verts[2] = pos + new Vector3(+halfGrid.x, 0, +halfGrid.y);
verts[3] = pos + new Vector3(+halfGrid.x, 0, -halfGrid.y);

Color color = Color.gray * (colorFlip ? 1 : 0.7f);

foreach (Vector3 player in players)
{
var gridRect = new Rect(x + offset.x, y + offset.y, gridSize.x, gridSize.y);
if (gridRect.Contains(player.ToXZ()))
{
// not too red
color = Color.red * 0.5f;
break;
}
}

// transparent
color.a = 0.6f;
Handles.DrawSolidRectangleWithOutline(verts, color, Color.black);
colorFlip = !colorFlip;
}
// if even we need to re-flip so that color is samme as last one, but will be different than above it
if (points.y % 2 == 0)
colorFlip = !colorFlip;
}
}

private void DrawBoundsHandle(SpatialHashSystem system)
{
boundsHandle.center = system.Bounds.center;
boundsHandle.size = system.Bounds.size;
EditorGUI.BeginChangeCheck();
boundsHandle.DrawHandle();
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(system, "Change Bounds");
system.Bounds.center = boundsHandle.center;
system.Bounds.size = boundsHandle.size;
}
}
}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Mirage.Logging;
using UnityEngine;
@@ -11,12 +12,15 @@ public class SpatialHashVisibility : NetworkVisibility
[Tooltip("How many grid away the player can be to see this object. Real distance is this mutlipled by SpatialHashSystem")]
public int GridVisibleRange = 1;

[ReadOnlyInspector]
public SpatialHashSystem System;

/// <param name="player">Network connection of a player.</param>
/// <returns>True if the player can see this object.</returns>
public override bool OnCheckObserver(INetworkPlayer player)
{
ThrowIfNoSystem();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should return and wait for system instead of throw. System can rebuild existing NI when it starts.

This is to better deal with spawning objects inside server.Started, the system might not be first in even list


if (player.Identity == null)
return false;

@@ -35,7 +39,17 @@ public override bool OnCheckObserver(INetworkPlayer player)
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
public override void OnRebuildObservers(HashSet<INetworkPlayer> observers, bool initialize)
{
ThrowIfNoSystem();

System.Grid.BuildObservers(observers, transform.position.ToXZ(), GridVisibleRange);
}

private void ThrowIfNoSystem()
{
if (System is null)
{
throw new InvalidOperationException("No SpatialHashSystem Set on SpatialHashVisibility. Add SpatialHashSystem to your NetworkManager before using this component.");
}
}
}
}