using System.Collections.Generic; using MoonWorks.Math.Fixed; namespace MoonWorks.Collision.Fixed { /// /// Used to quickly check if two shapes are potentially overlapping. /// /// The type that will be used to uniquely identify shape-transform pairs. public class SpatialHash2D where T : struct, System.IEquatable where U : IHasAABB2D { private readonly Fix64 cellSize; private readonly Dictionary> hashDictionary = new Dictionary>(); private readonly Dictionary IDLookup = new Dictionary(); public int MinX { get; private set; } = 0; public int MaxX { get; private set; } = 0; public int MinY { get; private set; } = 0; public int MaxY { get; private set; } = 0; private Queue> hashSetPool = new Queue>(); public SpatialHash2D(int cellSize) { this.cellSize = new Fix64(cellSize); } private (int, int) Hash(Vector2 position) { return ((int) (position.X / cellSize), (int) (position.Y / cellSize)); } /// /// Inserts an element into the SpatialHash. /// /// A unique ID for the shape-transform pair. /// /// /// A bitmask value specifying the groups this object belongs to. public void Insert(T id, U shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue) { var box = AABB2D.Transformed(shape.AABB, transform2D); var minHash = Hash(box.Min); var maxHash = Hash(box.Max); foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2)) { if (!hashDictionary.ContainsKey(key)) { hashDictionary.Add(key, new HashSet()); } hashDictionary[key].Add(id); IDLookup[id] = (shape, transform2D, collisionGroups); } MinX = System.Math.Min(MinX, minHash.Item1); MinY = System.Math.Min(MinY, minHash.Item2); MaxX = System.Math.Max(MaxX, maxHash.Item1); MaxY = System.Math.Max(MaxY, maxHash.Item2); } /// /// Retrieves all the potential collisions of a shape-transform pair. Excludes any shape-transforms with the given ID. /// public RetrieveEnumerator Retrieve(T id, V hasAABB, Transform2D transform2D, uint collisionMask = uint.MaxValue) where V : IHasAABB2D { var box = AABB2D.Transformed(hasAABB.AABB, transform2D); var (minX, minY) = Hash(box.Min); var (maxX, maxY) = Hash(box.Max); if (minX < MinX) { minX = MinX; } if (maxX > MaxX) { maxX = MaxX; } if (minY < MinY) { minY = MinY; } if (maxY > MaxY) { maxY = MaxY; } return new RetrieveEnumerator( this, Keys(minX, minY, maxX, maxY), id, collisionMask ); } /// /// Retrieves all the potential collisions of a shape-transform pair. /// public RetrieveEnumerator Retrieve(V hasAABB, Transform2D transform2D, uint collisionMask = uint.MaxValue) where V : IHasAABB2D { var box = AABB2D.Transformed(hasAABB.AABB, transform2D); return Retrieve(box, collisionMask); } /// /// Retrieves objects based on a pre-transformed AABB. /// /// A transformed AABB. /// public RetrieveEnumerator Retrieve(AABB2D aabb, uint collisionMask = uint.MaxValue) { var (minX, minY) = Hash(aabb.Min); var (maxX, maxY) = Hash(aabb.Max); if (minX < MinX) { minX = MinX; } if (maxX > MaxX) { maxX = MaxX; } if (minY < MinY) { minY = MinY; } if (maxY > MaxY) { maxY = MaxY; } return new RetrieveEnumerator( this, Keys(minX, minY, maxX, maxY), collisionMask ); } public void Update(T id, U shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue) { Remove(id); Insert(id, shape, transform2D, collisionGroups); } /// /// Removes a specific ID from the SpatialHash. /// public void Remove(T id) { var (shape, transform, collisionGroups) = IDLookup[id]; var box = AABB2D.Transformed(shape.AABB, transform); var minHash = Hash(box.Min); var maxHash = Hash(box.Max); foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2)) { if (hashDictionary.ContainsKey(key)) { hashDictionary[key].Remove(id); } } IDLookup.Remove(id); } /// /// Removes everything that has been inserted into the SpatialHash. /// public void Clear() { foreach (var hash in hashDictionary.Values) { hash.Clear(); } IDLookup.Clear(); } private static long MakeLong(int left, int right) { return ((long) left << 32) | ((uint) right); } internal static KeysEnumerator Keys(int minX, int minY, int maxX, int maxY) { return new KeysEnumerator(minX, minY, maxX, maxY); } internal HashSet AcquireHashSet() { if (hashSetPool.Count == 0) { hashSetPool.Enqueue(new HashSet()); } var hashSet = hashSetPool.Dequeue(); hashSet.Clear(); return hashSet; } internal void FreeHashSet(HashSet hashSet) { hashSetPool.Enqueue(hashSet); } internal ref struct KeysEnumerator { private int MinX; private int MinY; private int MaxX; private int MaxY; private int i, j; public KeysEnumerator GetEnumerator() => this; public KeysEnumerator(int minX, int minY, int maxX, int maxY) { MinX = minX; MinY = minY; MaxX = maxX; MaxY = maxY; i = minX; j = minY - 1; } public bool MoveNext() { if (j < MaxY) { j += 1; return true; } else if (i < MaxX) { i += 1; j = MinY; return true; } return false; } public long Current { get { return MakeLong(i, j); } } } public ref struct RetrieveEnumerator { public SpatialHash2D SpatialHash; private KeysEnumerator KeysEnumerator; private HashSet.Enumerator HashSetEnumerator; private bool HashSetEnumeratorActive; private HashSet Duplicates; private T? ID; private uint CollisionMask; public RetrieveEnumerator GetEnumerator() => this; internal RetrieveEnumerator( SpatialHash2D spatialHash, KeysEnumerator keysEnumerator, T id, uint collisionMask ) { SpatialHash = spatialHash; KeysEnumerator = keysEnumerator; HashSetEnumerator = default; HashSetEnumeratorActive = false; Duplicates = SpatialHash.AcquireHashSet(); ID = id; CollisionMask = collisionMask; } internal RetrieveEnumerator( SpatialHash2D spatialHash, KeysEnumerator keysEnumerator, uint collisionMask ) { SpatialHash = spatialHash; KeysEnumerator = keysEnumerator; HashSetEnumerator = default; HashSetEnumeratorActive = false; Duplicates = SpatialHash.AcquireHashSet(); ID = null; CollisionMask = collisionMask; } public bool MoveNext() { if (!HashSetEnumeratorActive || !HashSetEnumerator.MoveNext()) { if (!KeysEnumerator.MoveNext()) { SpatialHash.FreeHashSet(Duplicates); return false; } if (SpatialHash.hashDictionary.TryGetValue(KeysEnumerator.Current, out var hashset)) { HashSetEnumerator = hashset.GetEnumerator(); HashSetEnumeratorActive = true; } return MoveNext(); } // conditions var t = HashSetEnumerator.Current; var collisionGroups = SpatialHash.IDLookup[t].Item3; if (Duplicates.Contains(t)) { return MoveNext(); } if (ID.HasValue) { if (ID.Value.Equals(t) || (CollisionMask & collisionGroups) == 0) { return MoveNext(); } } Duplicates.Add(t); return true; } public (T, U, Transform2D, uint) Current { get { var t = HashSetEnumerator.Current; var (u, transform, groups) = SpatialHash.IDLookup[t]; return (t, u, transform, groups); } } } } }