From d9bedb4d8baf6b14ec76b88fe8c3c3aa063a8d0f Mon Sep 17 00:00:00 2001 From: Mikhail <99481254+DCFApixels@users.noreply.github.com> Date: Mon, 19 Jun 2023 01:36:36 +0800 Subject: [PATCH] create base types & utils --- src/EcsRelationWorld.cs | 6 + src/Relation.cs | 10 ++ src/RelationManager.cs | 103 +++++++++++++++ src/Utils/ArrayUtility.cs | 18 +++ src/Utils/IdsBasket.cs | 50 ++++++++ src/Utils/IdsLinkedList.cs | 255 +++++++++++++++++++++++++++++++++++++ src/Utils/SparseArray.cs | 212 ++++++++++++++++++++++++++++++ src/Utils/SparseArray64.cs | 223 ++++++++++++++++++++++++++++++++ 8 files changed, 877 insertions(+) create mode 100644 src/EcsRelationWorld.cs create mode 100644 src/Relation.cs create mode 100644 src/RelationManager.cs create mode 100644 src/Utils/ArrayUtility.cs create mode 100644 src/Utils/IdsBasket.cs create mode 100644 src/Utils/IdsLinkedList.cs create mode 100644 src/Utils/SparseArray.cs create mode 100644 src/Utils/SparseArray64.cs diff --git a/src/EcsRelationWorld.cs b/src/EcsRelationWorld.cs new file mode 100644 index 0000000..95d6bb3 --- /dev/null +++ b/src/EcsRelationWorld.cs @@ -0,0 +1,6 @@ +using DCFApixels.DragonECS; + +namespace DragonECS.DragonECS +{ + public sealed class EcsRelationWorld : EcsWorld { } +} diff --git a/src/Relation.cs b/src/Relation.cs new file mode 100644 index 0000000..d1dc6ca --- /dev/null +++ b/src/Relation.cs @@ -0,0 +1,10 @@ +using DCFApixels.DragonECS; + +namespace DragonECS.DragonECS +{ + internal readonly struct Relation : IEcsComponent + { + public readonly int entity; + public readonly int otherEntity; + } +} \ No newline at end of file diff --git a/src/RelationManager.cs b/src/RelationManager.cs new file mode 100644 index 0000000..e1d2b0e --- /dev/null +++ b/src/RelationManager.cs @@ -0,0 +1,103 @@ +using DCFApixels.DragonECS; +using DCFApixels.DragonECS.Relations.Utils; + +namespace DragonECS.DragonECS +{ + internal static class WorldRelationsMatrix + { + private static SparseArray64 _matrix; + + internal static bool HasRelation(EcsWorld world, EcsWorld otherWorld) => HasRelation(world.id, otherWorld.id); + internal static bool HasRelation(int worldID, int otherWorldID) => _matrix.Contains(worldID, otherWorldID); + + internal static RelationManager Register(EcsWorld world, EcsWorld otherWorld, EcsRelationWorld relationWorld) + { + int worldID = world.id; + int otherWorldID = otherWorld.id; +#if DEBUG + if (_matrix.Contains(worldID, otherWorldID)) + throw new EcsFrameworkException(); +#endif + RelationManager manager = new RelationManager(relationWorld, world, otherWorld); + _matrix[worldID, otherWorldID] = manager; + return manager; + } + internal static void Unregister(EcsWorld world, EcsWorld otherWorld) + { + int worldID = world.id; + int otherWorldID = otherWorld.id; + //var manager = _matrix[worldID, otherWorldID]; + _matrix.Remove(worldID, otherWorldID); + } + + public static RelationManager Get(EcsWorld world, EcsWorld otherWorld) + { +#if DEBUG + if (_matrix.Contains(world.id, otherWorld.id)) + throw new EcsFrameworkException(); +#endif + return _matrix[world.id, otherWorld.id]; + } + } + + public class RelationManager : IEcsPoolEventListener, IEcsWorldEventListener, IEcsEntityEventListener + { + private EcsRelationWorld _relationWorld; + + private EcsWorld _world; + private EcsWorld _otherWorld; + + private int[] _mapping = new int[256]; + private IdsLinkedList _idsBasket = new IdsLinkedList(256); + private EcsPool _relationsPool; + + public EcsWorld RelationWorld => _relationWorld; + public bool IsSolo => _world == _otherWorld; + + internal RelationManager(EcsRelationWorld relationWorld, EcsWorld world, EcsWorld otherWorld) + { + _relationWorld = relationWorld; + _world = world; + _otherWorld = otherWorld; + + _relationsPool = relationWorld.GetPool(); + _relationsPool.AddListener(this); + _relationWorld.AddListener(worldEventListener: this); + _relationWorld.AddListener(entityEventListener: this); + } + + void IEcsPoolEventListener.OnAdd(int entityID) + { + //_idsBasket. + } + void IEcsPoolEventListener.OnGet(int entityID) { } + void IEcsPoolEventListener.OnDel(int entityID) { } + + void IEcsWorldEventListener.OnWorldResize(int newSize) { } + void IEcsWorldEventListener.OnReleaseDelEntityBuffer(ReadOnlySpan buffer) { } + void IEcsWorldEventListener.OnWorldDestroy() { } + + void IEcsEntityEventListener.OnNewEntity(int entityID) { } + void IEcsEntityEventListener.OnDelEntity(int entityID) + { + + } + } + + + public static class WorldRelationExtensions + { + public static void SetRelationWith(this EcsWorld self, EcsWorld otherWorld) + { + WorldRelationsMatrix.Register(self, otherWorld, new EcsRelationWorld()); + } + public static void SetRelationWith(this EcsWorld self, EcsWorld otherWorld, EcsRelationWorld relationWorld) + { + WorldRelationsMatrix.Register(self, otherWorld, relationWorld); + } + public static void DelRelationWith(this EcsWorld self, EcsWorld otherWorld) + { + WorldRelationsMatrix.Unregister(self, otherWorld); + } + } +} diff --git a/src/Utils/ArrayUtility.cs b/src/Utils/ArrayUtility.cs new file mode 100644 index 0000000..24406d7 --- /dev/null +++ b/src/Utils/ArrayUtility.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace DCFApixels.DragonECS.Relations.Utils +{ + internal static class ArrayUtility + { + public static void Fill(T[] array, T value, int startIndex = 0, int length = -1) + { + if (length < 0) + length = array.Length; + else + length = startIndex + length; + for (int i = startIndex; i < length; i++) + array[i] = value; + } + } +} diff --git a/src/Utils/IdsBasket.cs b/src/Utils/IdsBasket.cs new file mode 100644 index 0000000..400e8cb --- /dev/null +++ b/src/Utils/IdsBasket.cs @@ -0,0 +1,50 @@ +using DCFApixels.DragonECS.Relations.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DragonECS.DragonECS +{ + internal class IdsBasket + { + private IdsLinkedList _linketdList = new IdsLinkedList(4); + private int[] _mapping; + private int[] _counts; + + public void Clear() + { + _linketdList.Clear(); + ArrayUtility.Fill(_mapping, 0, 0, _mapping.Length); + ArrayUtility.Fill(_counts, 0, 0, _counts.Length); + } + public void AddToHead(int headEntityID, int addedEntityID) + { + ref var nodeIndex = ref _mapping[headEntityID]; + if (nodeIndex <= 0) + { + nodeIndex = _linketdList.Add(addedEntityID); + } + else + { + _linketdList.InsertAfter(nodeIndex, addedEntityID); + } + _counts[headEntityID]++; + } + + public void DelHead(int headEntityID) + { + + } + + public IdsLinkedList.Span GetEntitiesFor(int entity) + { + ref var nodeIndex = ref _mapping[entity]; + if (nodeIndex <= 0) + return _linketdList.EmptySpan(); + else + return _linketdList.GetSpan(nodeIndex, _counts[entity]); + } + } +} diff --git a/src/Utils/IdsLinkedList.cs b/src/Utils/IdsLinkedList.cs new file mode 100644 index 0000000..e6675d5 --- /dev/null +++ b/src/Utils/IdsLinkedList.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace DCFApixels.DragonECS.Relations.Utils +{ + [DebuggerTypeProxy(typeof(DebuggerProxy))] + public class IdsLinkedList : IEnumerable + { + public const int Head = 0; + + private Node[] _nodes; + private int _count; + private int _lastNodeIndex; + + private int[] _recycledNodes = new int[4]; + private int _recycledNodesCount; + + #region Properties + public int Count => _count; + public int Capacity => _nodes.Length; + public int Last => _lastNodeIndex; + #endregion + + #region Constructors + public IdsLinkedList(int capacity) + { + _nodes = new Node[capacity + 10]; + Clear(); + } + #endregion + + public void Resize(int newCapacity) + { + Array.Resize(ref _nodes, newCapacity + 10); + } + + public void Clear() + { + for (int i = 0; i < _nodes.Length; i++) + _nodes[i].next = 0; + _lastNodeIndex = Head; + _count = 0; + } + + public void Set(int nodeIndex, int value) => _nodes[nodeIndex].value = value; + public int Get(int nodeIndex) => _nodes[nodeIndex].value; + + /// Insert after + /// new node index + public int InsertAfter(int nodeIndex, int value) + { + int newNodeIndex; + if (_recycledNodesCount > 0) + { + newNodeIndex = _recycledNodes[--_recycledNodesCount]; + _count++; + } + else + { + newNodeIndex = ++_count; + } + + ref Node prevNode = ref _nodes[nodeIndex]; + ref Node nextNode = ref _nodes[prevNode.next]; + + _nodes[newNodeIndex].Set(value, nextNode.prev, prevNode.next); + + prevNode.next = newNodeIndex; + nextNode.prev = newNodeIndex; + + _lastNodeIndex = newNodeIndex; + return newNodeIndex; + } + //public int InsertBefore(int nodeIndex, int value) { } + public void Remove(int nodeIndex) + { + if(nodeIndex <= 0) + throw new ArgumentOutOfRangeException(); + + ref var node = ref _nodes[nodeIndex]; + _nodes[node.next].prev = node.prev; + _nodes[node.prev].next = node.next; + + // node = Node.Empty; + + if (_recycledNodesCount >= _recycledNodes.Length) + Array.Resize(ref _recycledNodes, _recycledNodes.Length << 1); + _recycledNodes[_recycledNodesCount++] = nodeIndex; + _count--; + } + + public int Add(int id) => InsertAfter(_lastNodeIndex, id); + public ref readonly Node GetNode(int nodeIndex) => ref _nodes[nodeIndex]; + + public SpanEnumerator GetEnumerator() => new SpanEnumerator(_nodes, _nodes[Head].next, _count); + public Span GetSpan(int startNodeIndex, int count) => new Span(this, startNodeIndex, count); + public Span EmptySpan() => new Span(this, 0, 0); + + #region IEnumerable + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + #region Utils Node/Enumerator/Span + [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 8)] + public struct Node + { + public static readonly Node Empty = new Node() { value = 0, next = -1 }; + public int value; + /// next node index + public int next; + /// prev node index + public int prev; + public void Set(int value, int prev, int next) + { + this.value = value; + this.next = next; + this.prev = prev; + } + public override string ToString() => $"node(<:{prev} >:{next} v:{value})"; + } + + #region Enumerator/Span + public struct Enumerator : IEnumerator + { + private readonly Node[] _nodes; + private int _index; + private int _next; + public Enumerator(Node[] nodes) + { + _nodes = nodes; + _index = -1; + _next = _nodes[Head].next; + } + public int Current => _nodes[_index].value; + + + public bool MoveNext() + { + _index = _next; + _next = _nodes[_next].next; + return _index > 0; + } + + object IEnumerator.Current => Current; + void IDisposable.Dispose() { } + void IEnumerator.Reset() + { + _index = -1; + _next = Head; + } + } + public readonly ref struct Span + { + private readonly IdsLinkedList _source; + private readonly int _startNodeIndex; + private readonly int _count; + public Span(IdsLinkedList source, int startNodeIndex, int count) + { + _source = source; + _startNodeIndex = startNodeIndex; + _count = count; + } + public SpanEnumerator GetEnumerator() => new SpanEnumerator(_source._nodes, _startNodeIndex, _count); + } + public struct SpanEnumerator : IEnumerator + { + private readonly Node[] _nodes; + private int _index; + private int _count; + private int _next; + public SpanEnumerator(Node[] nodes, int startIndex, int count) + { + _nodes = nodes; + _index = -1; + _count = count; + _next = startIndex; + } + public int Current => _nodes[_index].value; + public bool MoveNext() + { + _index = _next; + _next = _nodes[_next].next; + return _index > 0 && _count-- > 0; + } + + object IEnumerator.Current => Current; + void IDisposable.Dispose() { } + void IEnumerator.Reset() + { + _index = -1; + _next = Head; + } + } + #endregion + + #endregion + + public class DebuggerProxy + { + private IdsLinkedList list; + public NodeInfo[] Nodes + { + get + { + var result = new NodeInfo[list.Count]; + + Node[] nodes = list._nodes; + int index = -1; + int count = list._count; + int next = list._nodes[Head].next; + + int i = 0; + while (true) + { + index = next; + next = nodes[next].next; + if (index > 0 && count-- > 0) + { } + else break; + + ref var node = ref nodes[index]; + result[i] = new NodeInfo(index, node.prev, node.next, node.value); + i++; + } + + return result; + } + } + public DebuggerProxy(IdsLinkedList list) + { + this.list = list; + } + + public struct NodeInfo + { + public int index; + public int prev; + public int next; + public int value; + public NodeInfo(int index, int prev, int next, int value) + { + this.index = index; + this.prev = prev; + this.next = next; + this.value = value; + } + public override string ToString() => $"[{index}] {prev}_{next} - {value}"; + } + } + } +} diff --git a/src/Utils/SparseArray.cs b/src/Utils/SparseArray.cs new file mode 100644 index 0000000..138f5d2 --- /dev/null +++ b/src/Utils/SparseArray.cs @@ -0,0 +1,212 @@ +//SparseArray. Analogous to Dictionary, but faster. +//Benchmark result of indexer.get speed test with 300 elements: +//[Dictinary: 5.786us] [SparseArray: 2.047us]. +using System; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DCFApixels.DragonECS.Relations.Utils +{ + public class SparseArray + { + public const int MIN_CAPACITY_BITS_OFFSET = 4; + public const int MIN_CAPACITY = 1 << MIN_CAPACITY_BITS_OFFSET; + private const int EMPTY = -1; + + private int[] _buckets = Array.Empty(); + private Entry[] _entries = Array.Empty(); + + private int _count; + + private int _freeList; + private int _freeCount; + + private int _modBitMask; + + #region Properties + public TValue this[int key] + { + get => _entries[FindEntry(key)].value; + set => Insert(key, value); + } + + public int Count => _count; + #endregion + + #region Constructors + public SparseArray(int minCapacity = MIN_CAPACITY) + { + minCapacity = NormalizeCapacity(minCapacity); + _buckets = new int[minCapacity]; + for (int i = 0; i < minCapacity; i++) + _buckets[i] = EMPTY; + _entries = new Entry[minCapacity]; + _modBitMask = (minCapacity - 1) & 0x7FFFFFFF; + } + #endregion + + #region Add + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(int key, TValue value) + { +#if DEBUG + if (Contains(key)) + throw new ArgumentException("Contains(hashKey) is true"); +#endif + Insert(key, value); + } + #endregion + + #region Find/Insert/Remove + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindEntry(int key) + { + for (int i = _buckets[key & _modBitMask]; i >= 0; i = _entries[i].next) + if (_entries[i].hashKey == key) return i; + return -1; + } + private void Insert(int key, TValue value) + { + int targetBucket = key & _modBitMask; + + for (int i = _buckets[targetBucket]; i >= 0; i = _entries[i].next) + { + if (_entries[i].hashKey == key) + { + _entries[i].value = value; + return; + } + } + + int index; + if (_freeCount > 0) + { + index = _freeList; + _freeList = _entries[index].next; + _freeCount--; + } + else + { + if (_count == _entries.Length) + { + Resize(); + targetBucket = key & _modBitMask; + } + index = _count++; + } + + _entries[index].next = _buckets[targetBucket]; + _entries[index].hashKey = key; + _entries[index].value = value; + _buckets[targetBucket] = index; + } + public bool Remove(int key) + { + int bucket = key & _modBitMask; + int last = -1; + for (int i = _buckets[bucket]; i >= 0; last = i, i = _entries[i].next) + { + if (_entries[i].hashKey == key) + { + if (last < 0) + { + _buckets[bucket] = _entries[i].next; + } + else + { + _entries[last].next = _entries[i].next; + } + _entries[i].next = _freeList; + _entries[i].hashKey = -1; + _entries[i].value = default; + _freeList = i; + _freeCount++; + return true; + } + } + return false; + } + #endregion + + #region TryGetValue + public bool TryGetValue(int key, out TValue value) + { + int index = FindEntry(key); + if (index < 0) + { + value = default; + return false; + } + value = _entries[index].value; + return true; + } + #endregion + + #region Contains + public bool Contains(int key) + { + return FindEntry(key) >= 0; + } + #endregion + + #region Clear + public void Clear() + { + if (_count > 0) + { + for (int i = 0; i < _buckets.Length; i++) + { + _buckets[i] = -1; + } + Array.Clear(_entries, 0, _count); + _count = 0; + } + } + #endregion + + #region Resize + private void Resize() + { + int newSize = _buckets.Length << 1; + _modBitMask = (newSize - 1) & 0x7FFFFFFF; + + Contract.Assert(newSize >= _entries.Length); + int[] newBuckets = new int[newSize]; + for (int i = 0; i < newBuckets.Length; i++) + newBuckets[i] = EMPTY; + + Entry[] newEntries = new Entry[newSize]; + Array.Copy(_entries, 0, newEntries, 0, _count); + for (int i = 0; i < _count; i++) + { + if (newEntries[i].hashKey >= 0) + { + int bucket = newEntries[i].hashKey % newSize; + newEntries[i].next = newBuckets[bucket]; + newBuckets[bucket] = i; + } + } + _buckets = newBuckets; + _entries = newEntries; + } + + private int NormalizeCapacity(int capacity) + { + int result = MIN_CAPACITY; + while (result < capacity) result <<= 1; + return result; + } + #endregion + + #region Utils + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Entry + { + public int next; // Index of next entry, -1 if last + public int hashKey; + public TValue value; + } + #endregion + } +} \ No newline at end of file diff --git a/src/Utils/SparseArray64.cs b/src/Utils/SparseArray64.cs new file mode 100644 index 0000000..55be571 --- /dev/null +++ b/src/Utils/SparseArray64.cs @@ -0,0 +1,223 @@ +//SparseArray64. Analogous to Dictionary, but faster. +//Benchmark result of indexer.get speed test with 300 elements: +//[Dictinary: 6.705us] [SparseArray64: 2.512us]. +using System; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DCFApixels.DragonECS.Relations.Utils +{ + public class SparseArray64 + { + public const int MIN_CAPACITY_BITS_OFFSET = 4; + public const int MIN_CAPACITY = 1 << MIN_CAPACITY_BITS_OFFSET; + private const int EMPTY = -1; + + private int[] _buckets = Array.Empty(); + private Entry[] _entries = Array.Empty(); + + private int _count; + + private int _freeList; + private int _freeCount; + + private int _modBitMask; + + #region Properties + public TValue this[long keyX, long keyY] + { + get => _entries[FindEntry(keyX + (keyY << 32))].value; + set => Insert(keyX + (keyY << 32), value); + } + public TValue this[long key] + { + get => _entries[FindEntry(key)].value; + set => Insert(key, value); + } + + public int Count => _count; + #endregion + + #region Constructors + public SparseArray64(int minCapacity = MIN_CAPACITY) + { + minCapacity = NormalizeCapacity(minCapacity); + _buckets = new int[minCapacity]; + for (int i = 0; i < minCapacity; i++) + _buckets[i] = EMPTY; + _entries = new Entry[minCapacity]; + _modBitMask = (minCapacity - 1) & 0x7FFFFFFF; + } + #endregion + + #region Add + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(long key, TValue value) + { +#if DEBUG + if (Contains(key)) + throw new ArgumentException("Contains(hashKey) is true"); +#endif + Insert(key, value); + } + #endregion + + #region Find/Insert/Remove + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindEntry(long key) + { + for (int i = _buckets[unchecked((int)key & _modBitMask)]; i >= 0; i = _entries[i].next) + if (_entries[i].hashKey == key) return i; + return -1; + } + private void Insert(long key, TValue value) + { + int targetBucket = unchecked((int)key & _modBitMask); + + for (int i = _buckets[targetBucket]; i >= 0; i = _entries[i].next) + { + if (_entries[i].hashKey == key) + { + _entries[i].value = value; + return; + } + } + + int index; + if (_freeCount > 0) + { + index = _freeList; + _freeList = _entries[index].next; + _freeCount--; + } + else + { + if (_count == _entries.Length) + { + Resize(); + targetBucket = unchecked((int)key & _modBitMask); + } + index = _count++; + } + + _entries[index].next = _buckets[targetBucket]; + _entries[index].hashKey = key; + _entries[index].value = value; + _buckets[targetBucket] = index; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(long keyX, long keyY) => Remove(keyX + (keyY << 32)); + public bool Remove(long key) + { + int bucket = unchecked((int)key & _modBitMask); + int last = -1; + for (int i = _buckets[bucket]; i >= 0; last = i, i = _entries[i].next) + { + if (_entries[i].hashKey == key) + { + if (last < 0) + { + _buckets[bucket] = _entries[i].next; + } + else + { + _entries[last].next = _entries[i].next; + } + _entries[i].next = _freeList; + _entries[i].hashKey = -1; + _entries[i].value = default; + _freeList = i; + _freeCount++; + return true; + } + } + return false; + } + #endregion + + #region TryGetValue + public bool TryGetValue(long key, out TValue value) + { + int index = FindEntry(key); + if (index < 0) + { + value = default; + return false; + } + value = _entries[index].value; + return true; + } + #endregion + + #region Contains + public bool Contains(long keyX, long keyY) + { + return FindEntry(keyX + (keyY << 32)) >= 0; + } + public bool Contains(long key) + { + return FindEntry(key) >= 0; + } + #endregion + + #region Clear + public void Clear() + { + if (_count > 0) + { + for (int i = 0; i < _buckets.Length; i++) + { + _buckets[i] = -1; + } + Array.Clear(_entries, 0, _count); + _count = 0; + } + } + #endregion + + #region Resize + private void Resize() + { + int newSize = _buckets.Length << 1; + _modBitMask = (newSize - 1) & 0x7FFFFFFF; + + Contract.Assert(newSize >= _entries.Length); + int[] newBuckets = new int[newSize]; + for (int i = 0; i < newBuckets.Length; i++) + newBuckets[i] = EMPTY; + + Entry[] newEntries = new Entry[newSize]; + Array.Copy(_entries, 0, newEntries, 0, _count); + for (int i = 0; i < _count; i++) + { + if (newEntries[i].hashKey >= 0) + { + int bucket = unchecked((int)newEntries[i].hashKey & _modBitMask); + newEntries[i].next = newBuckets[bucket]; + newBuckets[bucket] = i; + } + } + _buckets = newBuckets; + _entries = newEntries; + } + + private int NormalizeCapacity(int capacity) + { + int result = MIN_CAPACITY; + while (result < capacity) result <<= 1; + return result; + } + #endregion + + #region Utils + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Entry + { + public int next; // Index of next entry, -1 if last + public long hashKey; + public TValue value; + } + #endregion + } +} \ No newline at end of file