From a4a4e715d149f13f69b452aa1be89716f0525298 Mon Sep 17 00:00:00 2001 From: itsBuggingMe Date: Sun, 9 Mar 2025 21:12:23 -0400 Subject: [PATCH 1/8] inital work on ecs --- .../Core/Collections/_Generic/DenseSet.cs | 34 +++ .../Core/Collections/_Generic/FastStack.cs | 128 +++++++++ .../Core/Collections/_Generic/SparseSet.cs | 272 +++++++++--------- src/EndlessEscapade/Core/ECS/Archetype.cs | 75 +++++ src/EndlessEscapade/Core/ECS/ArchetypeID.cs | 18 ++ src/EndlessEscapade/Core/ECS/Component.cs | 17 ++ src/EndlessEscapade/Core/ECS/ComponentID.cs | 12 + .../Core/ECS/ComponentStorage.cs | 11 + src/EndlessEscapade/Core/ECS/Entity.cs | 8 + src/EndlessEscapade/Core/ECS/Sig.cs | 190 ++++++++++++ src/EndlessEscapade/Core/ECS/World.cs | 49 ++++ src/EndlessEscapade/Core/ThrowHelper.cs | 16 ++ src/EndlessEscapade/EndlessEscapade.csproj | 4 +- 13 files changed, 690 insertions(+), 144 deletions(-) create mode 100644 src/EndlessEscapade/Core/Collections/_Generic/DenseSet.cs create mode 100644 src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs create mode 100644 src/EndlessEscapade/Core/ECS/Archetype.cs create mode 100644 src/EndlessEscapade/Core/ECS/ArchetypeID.cs create mode 100644 src/EndlessEscapade/Core/ECS/Component.cs create mode 100644 src/EndlessEscapade/Core/ECS/ComponentID.cs create mode 100644 src/EndlessEscapade/Core/ECS/ComponentStorage.cs create mode 100644 src/EndlessEscapade/Core/ECS/Entity.cs create mode 100644 src/EndlessEscapade/Core/ECS/Sig.cs create mode 100644 src/EndlessEscapade/Core/ECS/World.cs create mode 100644 src/EndlessEscapade/Core/ThrowHelper.cs diff --git a/src/EndlessEscapade/Core/Collections/_Generic/DenseSet.cs b/src/EndlessEscapade/Core/Collections/_Generic/DenseSet.cs new file mode 100644 index 00000000..e993b79c --- /dev/null +++ b/src/EndlessEscapade/Core/Collections/_Generic/DenseSet.cs @@ -0,0 +1,34 @@ +using System.Numerics; + +namespace EndlessEscapade.Framework.Collections; + +public sealed class DenseSet +{ + private T[] _items; + + public DenseSet(int capacity = 4) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); + _items = new T[capacity]; + } + + public ref T this[int index] + { + get + { + var locItems = _items; + if ((uint)index < (uint)_items.Length) + { + return ref locItems[index]; + } + + return ref ResizeAndGet(index); + } + } + + private ref T ResizeAndGet(int index) + { + Array.Resize(ref _items, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + return ref _items[index]; + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs new file mode 100644 index 00000000..c7e82947 --- /dev/null +++ b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs @@ -0,0 +1,128 @@ +using System.Collections.Generic; +using System.Collections; +using System; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; + +namespace EndlessEscapade.Core.Collections._Generic; + +/// +/// This struct is meant to be used purely inside of classes as fields. +/// +/// It is a light wrapper over an array. It does not track versions. +public struct FastStack : IEnumerable, IEnumerable + where T : notnull +{ + private T[] _buffer; + private int _nextIndex; + + public FastStack() : this(4) + { + + } + + public FastStack(int initalCapacity) + { + ArgumentOutOfRangeException.ThrowIfNegative(initalCapacity); + _buffer = initalCapacity == 0 ? [] : new T[initalCapacity]; + _nextIndex = 0; + } + + public int Count => _nextIndex; + + public ref T this[int index] + { + get + { + if ((uint)index < (uint)_nextIndex) + { + return ref _buffer[index]; + } + + return ref Throw_OutOfRange(); + } + } + + public void Push(T item) + { + var buffer = _buffer; + if (_nextIndex < _buffer.Length) + { + buffer[_nextIndex++] = item; + } + + ResizeAndPush(item); + } + + public T Pop() + { + var buffer = _buffer; + if ((uint)(--_nextIndex) < buffer.Length) + { + ref T item = ref buffer[_nextIndex]; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + var returnValue = item; + item = default!; + return returnValue; + } + return item; + } + + return Throw_EmptyStack(); + } + + public bool TryPop([NotNullWhen(true)] out T? item) + { + var buffer = _buffer; + if ((uint)(--_nextIndex) < buffer.Length) + { + ref T slot = ref buffer[_nextIndex]; + item = slot; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + item = default!; + return true; + } + + item = default; + return false; + } + + public Span AsSpan() => _buffer.AsSpan(0, _nextIndex); + + private void ResizeAndPush(in T item) + { + Array.Resize(ref _buffer, _buffer.Length + (_buffer.Length >> 1)); + _buffer[_nextIndex++] = item; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private T Throw_EmptyStack() + { + throw new InvalidOperationException("Stack is empty!"); + } + [MethodImpl(MethodImplOptions.NoInlining)] + private ref T Throw_OutOfRange() + { + throw new ArgumentOutOfRangeException(); + } + + #region Enumerable + public Enumerator GetEnumerator() => new(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator(FastStack stack) : IEnumerator, IEnumerator + { + private T[] _buffer = stack._buffer; + private int _nextIndex = stack._nextIndex; + private int _currentIndex = -1; + public readonly T Current => _buffer[_currentIndex]; + readonly object IEnumerator.Current => Current; + + public readonly void Dispose() { } + public bool MoveNext() => ++_currentIndex < _nextIndex; + public void Reset() => _currentIndex = -1; + } + #endregion Enumerable +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs b/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs index efab27f3..f3e0c302 100644 --- a/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs +++ b/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs @@ -1,223 +1,211 @@ -using System.Collections; +using EndlessEscapade.Core; +using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; +using Terraria; +using Terraria.DataStructures; namespace EndlessEscapade.Framework.Collections; -public sealed class SparseSet : IEnumerable, IDisposable +public sealed class SparseSet : IEnumerable { /// /// Gets the number of elements that the can hold without resizing. /// - public int Capacity { get; private set; } - + public int Capacity => _dense.Length; + /// /// Gets the number of elements contained in the . /// - public int Count { get; private set; } + public int Count => _nextIndex; - private T[] data; + private int _nextIndex; - private int[] dense; - private int[] sparse; + private int _version; - public SparseSet(int capacity) - { - ArgumentOutOfRangeException.ThrowIfNegative(capacity, nameof(capacity)); - - Capacity = capacity; + private T[] _dense; - data = new T[capacity]; - dense = new int[capacity]; - sparse = new int[capacity]; - - Array.Fill(sparse, -1); - } + // this collection should never be empty + private int[] _sparse; + + private const string INVALID_ID = "ID not in sparse set!"; - public IEnumerator GetEnumerator() + public ref T this[int id] { - for (var i = 0; i < Capacity; i++) + get { - if (!Has(i)) - { - continue; - } - - yield return data[i]; + ref var index = ref EnsureSparseCapacityAndGetIndex(id); + + if (index == -1) + index = _nextIndex++; + + return ref EnsureDenseCapacityAndGetSlot(index); } } - IEnumerator IEnumerable.GetEnumerator() + public SparseSet(int capacity = 4) { - return GetEnumerator(); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); + + _dense = new T[capacity]; + _sparse = new int[capacity]; } + public IEnumerator GetEnumerator() => new SparseSetEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public ref T Get(int id) { - if (id < 0 || id >= Count) - { - throw new IndexOutOfRangeException($"Index {id} is out of range."); + var localSparse = _sparse; + if(!((uint)id < (uint)localSparse.Length)) + {//out of range + ThrowHelper.Throw_ArgumentOutOfRange(INVALID_ID, id); } + var index = localSparse[id]; - return ref data[sparse[id]]; - } - - public bool TryGet(int id, [MaybeNullWhen(false)] out T entity) - { - entity = default; - - if (!Has(id)) + var localDense = _dense; + if (!((uint)index < (uint)localDense.Length)) { - return false; + ThrowHelper.Throw_ArgumentOutOfRange(INVALID_ID, id); } - entity = Get(id); - - return true; + return ref localDense[index]; } - public bool Add(int id, T value) + public bool TryGet(int id, [MaybeNullWhen(false)] out T value) { - if (Has(id)) - { - return false; - } - - EnsureCapacity(id + 1); + var localSparse = _sparse; + if (!((uint)id < (uint)localSparse.Length)) + goto doesntExist; - data[Count] = value; - sparse[id] = Count; - dense[Count] = id; - - Count++; + var index = localSparse[id]; - return true; - } + var localDense = _dense; + if (!((uint)index < (uint)localDense.Length)) + goto doesntExist; - public void Set(int id, T value) - { - EnsureCapacity(id + 1); + value = localDense[index]; + return false; - data[id] = value; + //saves a bit of code size + doesntExist: + value = default; + return false; } + public void SetOrAdd(int id, T item) => this[id] = item; + public bool Remove(int id) { - if (!Has(id)) - { - return false; - } + int moveDownIndex = --_nextIndex; - var denseIndex = sparse[id]; + var localSparse = _sparse; - if (denseIndex == -1) - { + if (!((uint)id < (uint)localSparse.Length)) return false; - } - var index = sparse[id]; + int moveIntoIndex = localSparse[id]; - var lastCount = Count - 1; - var lastIndex = dense[lastCount]; + var localDense = _dense; + if (!((uint)moveIntoIndex < (uint)localDense.Length)) + return false;//here, moveIntoIndex should really only ever be -1. We check against len to elide bounds check + + ref T from = ref localDense[moveDownIndex]; + localDense[moveIntoIndex] = from; - if (index != lastCount) - { - data[index] = data[lastCount]; - dense[index] = lastIndex; - sparse[lastIndex] = index; - } - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - data[lastCount] = default!; - } - - sparse[id] = -1; - - Count--; + from = default!; return true; } public bool Has(int id) { - return id >= 0 && id < Capacity && sparse[id] != -1; + var sparse = _sparse; + if (!((uint)id < (uint)sparse.Length)) + return false; + return sparse[id] != -1; } - public void Resize(int size) + public void EnsureCapacity(int capacity) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size, nameof(size)); - - var shrinking = size < Count; - var growing = size > Count; - - if (shrinking) + if(_dense.Length < capacity) { - for (int i = size; i < Count; i++) - { - if (dense[i] < 0) - { - sparse[dense[i]] = -1; - } - } - - Count = size; + Array.Resize(ref _dense, capacity); } + } - Array.Resize(ref data, size); - Array.Resize(ref dense, size); - Array.Resize(ref sparse, size); + /// + /// Note: this span will become invalid on resize or add + /// + public Span AsSpan() => _dense.AsSpan(0, _nextIndex); - if (growing) - { - Array.Fill(sparse, -1, Count, size - Count); + public void Clear() + { + _nextIndex = 0; + _sparse.AsSpan().Fill(-1); + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + _dense.AsSpan().Clear(); + } - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - Array.Fill(dense, default, Count, size - Count); - } + private ref int EnsureSparseCapacityAndGetIndex(int id) + { + var localSparse = _sparse; + if((uint)id < (uint)localSparse.Length) + { + return ref localSparse[id]; } - Capacity = size; - } - - public void EnsureCapacity(int capacity) - { - ArgumentOutOfRangeException.ThrowIfNegative(capacity, nameof(capacity)); + return ref ResizeArrayAndGet(ref _sparse, id); - if (capacity <= Capacity) + static ref int ResizeArrayAndGet(ref int[] arr, int index) { - return; + int prevLen = arr.Length; + Array.Resize(ref arr, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + arr.AsSpan(prevLen).Fill(-1); + return ref arr[index]; } - - var newCapacity = Math.Max(1, Capacity); - - while (newCapacity <= capacity) + } + + private ref T EnsureDenseCapacityAndGetSlot(int index) + { + var localDense = _dense; + if ((uint)index < (uint)localDense.Length) { - newCapacity *= 2; + return ref localDense[index]; } - Resize(newCapacity); - } + return ref ResizeArrayAndGet(ref _dense, index); - public void Clear() - { - if (Count <= 0) + static ref T ResizeArrayAndGet(ref T[] arr, int index) { - return; + Array.Resize(ref arr, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + return ref arr[index]; } - - Array.Clear(data); - Array.Fill(sparse, -1); - - Count = 0; } - public void Dispose() + public struct SparseSetEnumerator(SparseSet set) : IEnumerator { - data = null!; - dense = null!; - sparse = null!; + private readonly SparseSet _toEnumerate = set; + private readonly int _version = set._version; + private int _index = -1; + + public readonly ref T Current => ref _toEnumerate._dense[_index]; + + public bool MoveNext() + { + if (_version != _toEnumerate._version) + ThrowHelper.Throw_InvalidOperation("Collection has been modified, cannot continue iteration."); + return ++_index < _toEnumerate._nextIndex; + } + + public void Reset() => _index = -1; + + readonly object? IEnumerator.Current => Current; + readonly T IEnumerator.Current => _toEnumerate._dense[_index]; + public readonly void Dispose() { } } } \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Archetype.cs b/src/EndlessEscapade/Core/ECS/Archetype.cs new file mode 100644 index 00000000..a2116362 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Archetype.cs @@ -0,0 +1,75 @@ +using EndlessEscapade.Core.Collections._Generic; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Data.Common; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Terraria; + +namespace EndlessEscapade.Core.ECS; + +public partial class Archetype +{ + private static int nextArchetypeID; + + private static FastStack> ArchetypeMetadata = new(); + private static readonly Dictionary<(ulong h1, ulong h2), ArchetypeID> existingArchetypeIDs = []; + + public static ArchetypeID GetArchetypeID() + where T : struct, IRec + { + default(T).AppendTypes(IRec.SharedTypeList); + + ulong hash1 = 0; + ulong hash2 = 0; + foreach (var type in IRec.SharedTypeList) + { + hash1 ^= (ulong)type.GetHashCode() * 98317U; + hash2 += (ulong)type.GetHashCode() * 53U; + } + IRec.SharedTypeList.Clear(); + + ref ArchetypeID id = ref CollectionsMarshal.GetValueRefOrAddDefault(existingArchetypeIDs, (hash1, hash2), out bool exists); + if (exists) + return id; + + ushort newRawId = checked((ushort)nextArchetypeID); + + return id = ArchetypeID.CreateNew(newRawId); + } + + public static Archetype Create(ArchetypeID id) + { + var arr = ArchetypeMetadata[id.GetRawValue()]; + ComponentStorage[] storages = new ComponentStorage[arr.Length]; + for(int i = 0; i < arr.Length; i++) + { + storages[i] = Component.Create(arr[i]); + } + + //TODO: index map + throw new NotImplementedException(); + return new Archetype(storages, null!); + } +} + +public partial class Archetype(ComponentStorage[] storages, byte[] indexMap) +{ + private readonly ComponentStorage[] _storages = storages; + private readonly byte[] _indexMap = indexMap; + + + public int Create() + { + throw new NotImplementedException(); + } + + public ref T GetComponent(int index) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/ArchetypeID.cs b/src/EndlessEscapade/Core/ECS/ArchetypeID.cs new file mode 100644 index 00000000..0e0976e5 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/ArchetypeID.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS; + +public struct ArchetypeID +{ + private ushort _value; + + + + + internal ushort GetRawValue() => _value; + internal static ArchetypeID CreateNew(ushort value) => new() { _value = value }; +} diff --git a/src/EndlessEscapade/Core/ECS/Component.cs b/src/EndlessEscapade/Core/ECS/Component.cs new file mode 100644 index 00000000..7d9dcb2a --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Component.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS; +internal class Component +{ + public static ComponentStorage Create() => throw new NotImplementedException(); +} + +internal class Component +{ + public static ComponentStorage Create(ComponentID componentID) => throw new NotImplementedException(); +} + diff --git a/src/EndlessEscapade/Core/ECS/ComponentID.cs b/src/EndlessEscapade/Core/ECS/ComponentID.cs new file mode 100644 index 00000000..0f6bb11e --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/ComponentID.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS; + +internal struct ComponentID +{ + private ushort _value; +} diff --git a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs new file mode 100644 index 00000000..b83f7539 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS; + +public abstract class ComponentStorage +{ +} diff --git a/src/EndlessEscapade/Core/ECS/Entity.cs b/src/EndlessEscapade/Core/ECS/Entity.cs new file mode 100644 index 00000000..5a6ab87d --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Entity.cs @@ -0,0 +1,8 @@ +namespace EndlessEscapade.Core.ECS; + +public struct Entity(int entityID, int entityVersion, World world) +{ + private int EntityID = entityID; + private int EntityVersion = entityVersion; + private World World = world; +} diff --git a/src/EndlessEscapade/Core/ECS/Sig.cs b/src/EndlessEscapade/Core/ECS/Sig.cs new file mode 100644 index 00000000..20a104d0 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Sig.cs @@ -0,0 +1,190 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace EndlessEscapade.Core.ECS; + +public struct Rec : IRec +{ + private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); + public readonly ArchetypeID ArchetypeID => CachedArchetypeID; + + public T Item1; + + public T1 GetAt(int index) + { + if (index != 0) + throw new IndexOutOfRangeException(); + return (T1)(object)Item1!; + } + + public void AppendTypes(List appendTo) => appendTo.Add(typeof(T)); + + public void SetArchetype(Archetype archetype, int index) => archetype.GetComponent(index) = Item1; + + public static implicit operator Rec(T val) => new() { Item1 = val }; +} + +public struct Rec : IRec +{ + private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); + public readonly ArchetypeID ArchetypeID => CachedArchetypeID; + + public T1 Item1; + public T2 Item2; + + public T GetAt(int index) + { + return index switch + { + 0 => (T)(object)Item1!, + 1 => (T)(object)Item2!, + _ => throw new IndexOutOfRangeException() + }; + } + + public void AppendTypes(List appendTo) + { + appendTo.Add(typeof(T1)); + appendTo.Add(typeof(T2)); + } + + public void SetArchetype(Archetype archetype, int index) + { + archetype.GetComponent(index) = Item1; + archetype.GetComponent(index) = Item2; + } + + public static implicit operator Rec((T1, T2) val) => new() { Item1 = val.Item1, Item2 = val.Item2 }; +} + +public struct Rec : IRec +{ + private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); + public readonly ArchetypeID ArchetypeID => CachedArchetypeID; + + public T1 Item1; + public T2 Item2; + public T3 Item3; + + public T GetAt(int index) + { + return index switch + { + 0 => (T)(object)Item1!, + 1 => (T)(object)Item2!, + 2 => (T)(object)Item3!, + _ => throw new IndexOutOfRangeException() + }; + } + + public void AppendTypes(List appendTo) + { + appendTo.Add(typeof(T1)); + appendTo.Add(typeof(T2)); + appendTo.Add(typeof(T3)); + } + + public void SetArchetype(Archetype archetype, int index) + { + archetype.GetComponent(index) = Item1; + archetype.GetComponent(index) = Item2; + archetype.GetComponent(index) = Item3; + } + + public static implicit operator Rec((T1, T2, T3) val) => new() { Item1 = val.Item1, Item2 = val.Item2, Item3 = val.Item3 }; +} + +public struct Rec : IRec +{ + private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); + public readonly ArchetypeID ArchetypeID => CachedArchetypeID; + + public T1 Item1; + public T2 Item2; + public T3 Item3; + public T4 Item4; + + public T GetAt(int index) + { + return index switch + { + 0 => (T)(object)Item1!, + 1 => (T)(object)Item2!, + 2 => (T)(object)Item3!, + 3 => (T)(object)Item4!, + _ => throw new IndexOutOfRangeException() + }; + } + + public void AppendTypes(List appendTo) + { + appendTo.Add(typeof(T1)); + appendTo.Add(typeof(T2)); + appendTo.Add(typeof(T3)); + appendTo.Add(typeof(T4)); + } + + public void SetArchetype(Archetype archetype, int index) + { + archetype.GetComponent(index) = Item1; + archetype.GetComponent(index) = Item2; + archetype.GetComponent(index) = Item3; + archetype.GetComponent(index) = Item4; + } + + public static implicit operator Rec((T1, T2, T3, T4) val) => new() { Item1 = val.Item1, Item2 = val.Item2, Item3 = val.Item3, Item4 = val.Item4 }; +} + +public struct Rec : IRec where TRest : IRec +{ + private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); + public readonly ArchetypeID ArchetypeID => CachedArchetypeID; + + public T1 Item1; + public T2 Item2; + public T3 Item3; + public T4 Item4; + public TRest Rest; + + public T GetAt(int index) + { + return index switch + { + 0 => (T)(object)Item1!, + 1 => (T)(object)Item2!, + 2 => (T)(object)Item3!, + 3 => (T)(object)Item4!, + _ => Rest.GetAt(index - 4) + }; + } + + public void AppendTypes(List appendTo) + { + appendTo.Add(typeof(T1)); + appendTo.Add(typeof(T2)); + appendTo.Add(typeof(T3)); + appendTo.Add(typeof(T4)); + Rest.AppendTypes(appendTo); + } + + public void SetArchetype(Archetype archetype, int index) + { + archetype.GetComponent(index) = Item1; + archetype.GetComponent(index) = Item2; + archetype.GetComponent(index) = Item3; + archetype.GetComponent(index) = Item4; + + Rest.SetArchetype(archetype, index); + } +} + + +public interface IRec +{ + ArchetypeID ArchetypeID { get; } + public T GetAt(int index); + public void AppendTypes(List appendTo); + public void SetArchetype(Archetype archetype, int index); + [ThreadStatic] + public static readonly List SharedTypeList = []; +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/World.cs b/src/EndlessEscapade/Core/ECS/World.cs new file mode 100644 index 00000000..8ad32daa --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/World.cs @@ -0,0 +1,49 @@ +using EndlessEscapade.Core.Collections._Generic; +using EndlessEscapade.Framework.Collections; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS; + +public class World +{ + //archetypeID -> Archetype + private SparseSet _archetypes = new(); + + private Dictionary<(bool IsAddAction, Type Delta, Archetype Archetype), Archetype> _archetypeGraph = []; + private Dictionary, HashSet> QueryCache = []; + + private DenseSet _table = new(); + private FastStack<(int Entity, int Version)> _recycledIDs = new FastStack<(int Entity, int Version)>(); + private int _nextID; + + public Entity Create(in T tuple) + where T : struct, IRec + { + (int id, int version) = _recycledIDs.TryPop(out var newId) ? newId : (_nextID++, 1); + + var toArchetypeID = tuple.ArchetypeID; + var archetype = _archetypes[toArchetypeID.GetRawValue()] ??= Archetype.Create(toArchetypeID); + + int index = archetype.Create(); + + _table[id] = new EntityLocation(archetype, id, version); + + //set components + tuple.SetArchetype(archetype, index); + + return new Entity(id, version, this); + } + + internal struct EntityLocation(Archetype archetype, int entityID, int version) + { + internal Archetype Archetype = archetype; + internal int EntityID = entityID; + internal int Version = version; + } +} diff --git a/src/EndlessEscapade/Core/ThrowHelper.cs b/src/EndlessEscapade/Core/ThrowHelper.cs new file mode 100644 index 00000000..9b9e35cc --- /dev/null +++ b/src/EndlessEscapade/Core/ThrowHelper.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace EndlessEscapade.Core; + +internal static class ThrowHelper +{ + public static void Throw_ArgumentOutOfRange(string message, int index, [CallerArgumentExpression(nameof(index))] string? paramName = null) + { + throw new ArgumentOutOfRangeException(paramName, index, message); + } + + public static void Throw_InvalidOperation(string message) + { + throw new InvalidOperationException(message); + } +} diff --git a/src/EndlessEscapade/EndlessEscapade.csproj b/src/EndlessEscapade/EndlessEscapade.csproj index 4b3fbffc..8781cb4b 100644 --- a/src/EndlessEscapade/EndlessEscapade.csproj +++ b/src/EndlessEscapade/EndlessEscapade.csproj @@ -1,6 +1,6 @@  - + true @@ -14,7 +14,7 @@ - + From e0cbe542bccfc04b99ba2017995dceafb7ea95ed Mon Sep 17 00:00:00 2001 From: itsBuggingMe Date: Mon, 10 Mar 2025 10:52:31 -0400 Subject: [PATCH 2/8] variadic world creation --- .../Core/Collections/_Generic/FastStack.cs | 8 ++ src/EndlessEscapade/Core/ECS/Archetype.cs | 40 ++++---- .../Core/ECS/ComponentStorage.cs | 8 ++ .../Core/ECS/EntityTemplate.cs | 97 +++++++++++++++++++ src/EndlessEscapade/Core/ECS/World.cs | 21 ++-- 5 files changed, 144 insertions(+), 30 deletions(-) create mode 100644 src/EndlessEscapade/Core/ECS/EntityTemplate.cs diff --git a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs index c7e82947..606f5d6d 100644 --- a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs +++ b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; using System.Diagnostics.CodeAnalysis; +using System.Security; namespace EndlessEscapade.Core.Collections._Generic; @@ -107,6 +108,13 @@ private ref T Throw_OutOfRange() throw new ArgumentOutOfRangeException(); } + public void Clear() + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + AsSpan().Clear(); + _nextIndex = 0; + } + #region Enumerable public Enumerator GetEnumerator() => new(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/EndlessEscapade/Core/ECS/Archetype.cs b/src/EndlessEscapade/Core/ECS/Archetype.cs index a2116362..8f986c61 100644 --- a/src/EndlessEscapade/Core/ECS/Archetype.cs +++ b/src/EndlessEscapade/Core/ECS/Archetype.cs @@ -16,24 +16,22 @@ public partial class Archetype { private static int nextArchetypeID; - private static FastStack> ArchetypeMetadata = new(); - private static readonly Dictionary<(ulong h1, ulong h2), ArchetypeID> existingArchetypeIDs = []; + private static FastStack<(ImmutableArray Components, byte[] IndexMap)> archetypeMetadata = new(); + private static readonly Dictionary<(ulong h1, ulong h2), ArchetypeID> ExistingArchetypeIDs = []; - public static ArchetypeID GetArchetypeID() - where T : struct, IRec + public static ArchetypeID GetArchetypeID(Span types) { - default(T).AppendTypes(IRec.SharedTypeList); + ArgumentOutOfRangeException.ThrowIfGreaterThan(types.Length, byte.MaxValue); ulong hash1 = 0; ulong hash2 = 0; - foreach (var type in IRec.SharedTypeList) + foreach (var type in types) { hash1 ^= (ulong)type.GetHashCode() * 98317U; hash2 += (ulong)type.GetHashCode() * 53U; } - IRec.SharedTypeList.Clear(); - ref ArchetypeID id = ref CollectionsMarshal.GetValueRefOrAddDefault(existingArchetypeIDs, (hash1, hash2), out bool exists); + ref ArchetypeID id = ref CollectionsMarshal.GetValueRefOrAddDefault(ExistingArchetypeIDs, (hash1, hash2), out bool exists); if (exists) return id; @@ -44,16 +42,16 @@ public static ArchetypeID GetArchetypeID() public static Archetype Create(ArchetypeID id) { - var arr = ArchetypeMetadata[id.GetRawValue()]; - ComponentStorage[] storages = new ComponentStorage[arr.Length]; - for(int i = 0; i < arr.Length; i++) + ref var arr = ref archetypeMetadata[id.GetRawValue()]; + var components = arr.Components; + + ComponentStorage[] storages = new ComponentStorage[components.Length]; + for(int i = 0; i < components.Length; i++) { - storages[i] = Component.Create(arr[i]); + storages[i] = Component.Create(components[i]); } - //TODO: index map - throw new NotImplementedException(); - return new Archetype(storages, null!); + return new Archetype(storages, arr.IndexMap); } } @@ -61,15 +59,17 @@ public partial class Archetype(ComponentStorage[] storages, byte[] indexMap) { private readonly ComponentStorage[] _storages = storages; private readonly byte[] _indexMap = indexMap; - + private int _nextIndex; + private int _capacity; public int Create() { throw new NotImplementedException(); - } + if(_nextIndex++ == _capacity) + { - public ref T GetComponent(int index) - { - throw new NotImplementedException(); + } } + + public ref T GetComponent(int index) => ref ((ComponentStorage)_storages[_indexMap[index]])[index]; } \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs index b83f7539..308a75dd 100644 --- a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs +++ b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs @@ -9,3 +9,11 @@ namespace EndlessEscapade.Core.ECS; public abstract class ComponentStorage { } + +public class ComponentStorage : ComponentStorage +{ + public ref T this[int index] + { + get => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/EntityTemplate.cs b/src/EndlessEscapade/Core/ECS/EntityTemplate.cs new file mode 100644 index 00000000..4f99f467 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/EntityTemplate.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EndlessEscapade.Core.Collections._Generic; + +namespace EndlessEscapade.Core.ECS; + +public struct Add : IEntityTemplate + where TRest : IEntityTemplate +{ + public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID>(); + private static ArchetypeID? cache; + + + public World World => Rest.World; + public Entity Entity => World.SetComponents(ref this, ArchetypeID); + + + public TRest Rest; + public T Value; + + + public Add, TN> Set(TN value) => new() + { + Rest = this, + Value = value + }; + + public void AppendTypes(ref FastStack types) + { + types.Push(typeof(TRest)); + Rest.AppendTypes(ref types); + } + + public void SetComponents(Archetype archetype, int index) + { + archetype.GetComponent(index) = Value; + Rest.SetComponents(archetype, index); + } +} + +public struct EntityTemplate : IEntityTemplate +{ + public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID>(); + private static ArchetypeID? cache; + + public required World World { get; init; } + public Entity Entity => World.SetComponents(ref this, ArchetypeID); + + + public T Value; + + + public Add, TN> Set(TN value) => new() + { + Rest = this, + Value = value, + }; + + public void AppendTypes(ref FastStack types) => types.Push(typeof(T)); + + public void SetComponents(Archetype archetype, int index) => archetype.GetComponent(index) = Value; +} + +public struct EntityTemplate +{ + public World World; + public EntityTemplate Set(T value) => new() + { + World = World, + Value = value, + }; +} + +public interface IEntityTemplate +{ + public void AppendTypes(ref FastStack types); + public void SetComponents(Archetype archetype, int index); + public Entity Entity { get; } + public ArchetypeID ArchetypeID { get; } + public World World { get; } + + static ArchetypeID CalculateArchetypeID() + where T : struct, IEntityTemplate + { + ref FastStack types = ref sharedStack; + default(T)!.AppendTypes(ref types); + var id = Archetype.GetArchetypeID(types.AsSpan()); + types.Clear(); + return id; + } + + [ThreadStatic] + private static FastStack sharedStack = new(); +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/World.cs b/src/EndlessEscapade/Core/ECS/World.cs index 8ad32daa..c731e593 100644 --- a/src/EndlessEscapade/Core/ECS/World.cs +++ b/src/EndlessEscapade/Core/ECS/World.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Linq; using System.Runtime.CompilerServices; +using System.Security.Principal; using System.Text; using System.Threading.Tasks; @@ -22,28 +23,28 @@ public class World private FastStack<(int Entity, int Version)> _recycledIDs = new FastStack<(int Entity, int Version)>(); private int _nextID; - public Entity Create(in T tuple) - where T : struct, IRec + public Entity SetComponents(ref readonly T template, ArchetypeID archetypeID) + where T : IEntityTemplate { (int id, int version) = _recycledIDs.TryPop(out var newId) ? newId : (_nextID++, 1); - var toArchetypeID = tuple.ArchetypeID; - var archetype = _archetypes[toArchetypeID.GetRawValue()] ??= Archetype.Create(toArchetypeID); + var archetype = _archetypes[archetypeID.GetRawValue()] ??= Archetype.Create(archetypeID); - int index = archetype.Create(); + var index = archetype.Create(); - _table[id] = new EntityLocation(archetype, id, version); + _table[id] = new EntityLocation(archetype, index, version); - //set components - tuple.SetArchetype(archetype, index); + template.SetComponents(archetype, index); return new Entity(id, version, this); } - internal struct EntityLocation(Archetype archetype, int entityID, int version) + public EntityTemplate Entity() => new() { World = this }; + + internal struct EntityLocation(Archetype archetype, int index, int version) { internal Archetype Archetype = archetype; - internal int EntityID = entityID; + internal int Index = index; internal int Version = version; } } From 26fb80209994f7ef592d1e8637e2773632add89b Mon Sep 17 00:00:00 2001 From: Paperclip Date: Mon, 10 Mar 2025 15:47:16 +0000 Subject: [PATCH 3/8] entity methods --- src/EndlessEscapade/Core/ECS/Archetype.cs | 17 ++++--- src/EndlessEscapade/Core/ECS/Component.cs | 1 + .../Core/ECS/ComponentStorage.cs | 1 + src/EndlessEscapade/Core/ECS/Entity.cs | 44 +++++++++++++++++-- src/EndlessEscapade/Core/ECS/EntityLight.cs | 2 + src/EndlessEscapade/Core/ECS/Ref.cs | 14 ++++++ src/EndlessEscapade/Core/ECS/Sig.cs | 3 +- src/EndlessEscapade/Core/ECS/World.cs | 20 +++++++-- 8 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 src/EndlessEscapade/Core/ECS/EntityLight.cs create mode 100644 src/EndlessEscapade/Core/ECS/Ref.cs diff --git a/src/EndlessEscapade/Core/ECS/Archetype.cs b/src/EndlessEscapade/Core/ECS/Archetype.cs index 8f986c61..369d1136 100644 --- a/src/EndlessEscapade/Core/ECS/Archetype.cs +++ b/src/EndlessEscapade/Core/ECS/Archetype.cs @@ -57,19 +57,26 @@ public static Archetype Create(ArchetypeID id) public partial class Archetype(ComponentStorage[] storages, byte[] indexMap) { - private readonly ComponentStorage[] _storages = storages; + internal readonly ComponentStorage[] Storages = storages; private readonly byte[] _indexMap = indexMap; + private readonly EntityLight[] _entities = []; private int _nextIndex; - private int _capacity; - public int Create() { throw new NotImplementedException(); - if(_nextIndex++ == _capacity) + if(_nextIndex++ == _entities.Length) { } } - public ref T GetComponent(int index) => ref ((ComponentStorage)_storages[_indexMap[index]])[index]; + public int Delete(int index) + { + throw new NotImplementedException(); + } + + public ref T GetComponent(int index) => ref ((ComponentStorage)Storages[_indexMap[Component.ID.GetRawValue()]])[index]; + public ref T GetComponentKnownComponentStorageIndex(int index, int storageIndex) => + ref ((ComponentStorage)Storages[storageIndex])[index]; + public bool HasComponent(out int index) => (index = _indexMap[Component.ID.GetRawValue()]) != -1; } \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Component.cs b/src/EndlessEscapade/Core/ECS/Component.cs index 7d9dcb2a..b78eba6e 100644 --- a/src/EndlessEscapade/Core/ECS/Component.cs +++ b/src/EndlessEscapade/Core/ECS/Component.cs @@ -7,6 +7,7 @@ namespace EndlessEscapade.Core.ECS; internal class Component { + public static readonly ComponentID ID = default; public static ComponentStorage Create() => throw new NotImplementedException(); } diff --git a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs index 308a75dd..c8b0fb41 100644 --- a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs +++ b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs @@ -8,6 +8,7 @@ namespace EndlessEscapade.Core.ECS; public abstract class ComponentStorage { + public abstract void Delete(int archetypeID); } public class ComponentStorage : ComponentStorage diff --git a/src/EndlessEscapade/Core/ECS/Entity.cs b/src/EndlessEscapade/Core/ECS/Entity.cs index 5a6ab87d..bfae014d 100644 --- a/src/EndlessEscapade/Core/ECS/Entity.cs +++ b/src/EndlessEscapade/Core/ECS/Entity.cs @@ -1,8 +1,44 @@ namespace EndlessEscapade.Core.ECS; -public struct Entity(int entityID, int entityVersion, World world) +public readonly struct Entity(int entityID, int entityVersion, World world) { - private int EntityID = entityID; - private int EntityVersion = entityVersion; - private World World = world; + public readonly int EntityID = entityID; + public readonly int EntityVersion = entityVersion; + public readonly World World = world; + + public readonly bool Has() + { + var location = World.Table[EntityID]; + return location.Version != EntityVersion && location.Archetype.HasComponent(out _); + } + + public readonly ref T Get() + { + var location = World.Table[EntityID]; + if(location.Version != EntityVersion) + ThrowHelper.Throw_InvalidOperation(EntityIsDead); + return ref location.Archetype.GetComponent(location.Index); + } + + public readonly bool TryGet(out Ref item) + { + var location = World.Table[EntityID]; + if(location.Version != EntityVersion) + goto noComponent; + + if(!location.Archetype.HasComponent(out int storageIndex)) + goto noComponent; + + return ref location.Archetype.GetComponentKnownComponentStorageIndex( + location.Index, + storageIndex); + + noComponent: + item = default!; + return false; + } + + public bool Delete() => World.Delete(this); + + private const string EntityIsDead = "Entity is Dead"; } diff --git a/src/EndlessEscapade/Core/ECS/EntityLight.cs b/src/EndlessEscapade/Core/ECS/EntityLight.cs new file mode 100644 index 00000000..1896baf4 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/EntityLight.cs @@ -0,0 +1,2 @@ +namespace EndlessEscapade.Core.ECS; +public record struct EntityLight(int Index, int Version); \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Ref.cs b/src/EndlessEscapade/Core/ECS/Ref.cs new file mode 100644 index 00000000..e2f0593f --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Ref.cs @@ -0,0 +1,14 @@ +using EndlessEscapade.Core.Collections._Generic; +using EndlessEscapade.Framework.Collections; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace EndlessEscapade.Core.ECS; + +public ref struct Ref +{ + public ref T Ref; +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Sig.cs b/src/EndlessEscapade/Core/ECS/Sig.cs index 20a104d0..0fde2ff4 100644 --- a/src/EndlessEscapade/Core/ECS/Sig.cs +++ b/src/EndlessEscapade/Core/ECS/Sig.cs @@ -2,7 +2,7 @@ using System.Diagnostics; namespace EndlessEscapade.Core.ECS; - +//todo: agg inline all public struct Rec : IRec { private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); @@ -18,7 +18,6 @@ public T1 GetAt(int index) } public void AppendTypes(List appendTo) => appendTo.Add(typeof(T)); - public void SetArchetype(Archetype archetype, int index) => archetype.GetComponent(index) = Item1; public static implicit operator Rec(T val) => new() { Item1 = val }; diff --git a/src/EndlessEscapade/Core/ECS/World.cs b/src/EndlessEscapade/Core/ECS/World.cs index c731e593..9fb44836 100644 --- a/src/EndlessEscapade/Core/ECS/World.cs +++ b/src/EndlessEscapade/Core/ECS/World.cs @@ -19,8 +19,8 @@ public class World private Dictionary<(bool IsAddAction, Type Delta, Archetype Archetype), Archetype> _archetypeGraph = []; private Dictionary, HashSet> QueryCache = []; - private DenseSet _table = new(); - private FastStack<(int Entity, int Version)> _recycledIDs = new FastStack<(int Entity, int Version)>(); + internal DenseSet Table = new(); + private FastStack _recycledIDs = new FastStack(); private int _nextID; public Entity SetComponents(ref readonly T template, ArchetypeID archetypeID) @@ -32,7 +32,7 @@ public Entity SetComponents(ref readonly T template, ArchetypeID archetypeID) var index = archetype.Create(); - _table[id] = new EntityLocation(archetype, index, version); + Table[id] = new EntityLocation(archetype, index, version); template.SetComponents(archetype, index); @@ -41,10 +41,24 @@ public Entity SetComponents(ref readonly T template, ArchetypeID archetypeID) public EntityTemplate Entity() => new() { World = this }; + public bool Delete(Entity entity) + { + ref var location = ref Table[entity.EntityID]; + if(location.Version != entity.EntityVersion) + return false; + foreach(var storage in location.Archetype.Storages) + storage.Delete(location.Index); + throw new NotImplementedException(); + _recycledIDs.Push(new(entity.EntityID, entity.EntityVersion)); + location = EntityLocation.Default; + } + internal struct EntityLocation(Archetype archetype, int index, int version) { internal Archetype Archetype = archetype; internal int Index = index; internal int Version = version; + + public static EntityLocation Default = new EntityLocation(null!, -1, -1); } } From 0fe0976e47e457a94f6f109d42c9213ba969d1f7 Mon Sep 17 00:00:00 2001 From: Paperclip Date: Mon, 10 Mar 2025 19:16:20 +0000 Subject: [PATCH 4/8] component storage methods --- src/EndlessEscapade/Core/ECS/Archetype.cs | 16 +++-- .../Core/ECS/ComponentStorage.cs | 65 ++++++++++++++++++- src/EndlessEscapade/Core/ECS/World.cs | 13 ++-- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/EndlessEscapade/Core/ECS/Archetype.cs b/src/EndlessEscapade/Core/ECS/Archetype.cs index 369d1136..1a993bcc 100644 --- a/src/EndlessEscapade/Core/ECS/Archetype.cs +++ b/src/EndlessEscapade/Core/ECS/Archetype.cs @@ -59,20 +59,28 @@ public partial class Archetype(ComponentStorage[] storages, byte[] indexMap) { internal readonly ComponentStorage[] Storages = storages; private readonly byte[] _indexMap = indexMap; - private readonly EntityLight[] _entities = []; + private readonly ComponentStorage _entities = new(); private int _nextIndex; public int Create() { throw new NotImplementedException(); - if(_nextIndex++ == _entities.Length) + if(_nextIndex++ == _entities.Capacity) { } } - public int Delete(int index) + /// + /// Moves the top entity's components into and clears the top slot if needed. + /// + /// The entity id and version of the entity that was moved down + public EntityLight Delete(int index) { - throw new NotImplementedException(); + foreach(var stor in Storages) + stor.Delete(index); + var @return = _entities[_entities.Capacity - 1]; + _entities.Delete(index); + return @return; } public ref T GetComponent(int index) => ref ((ComponentStorage)Storages[_indexMap[Component.ID.GetRawValue()]])[index]; diff --git a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs index c8b0fb41..7c5538fb 100644 --- a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs +++ b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs @@ -8,13 +8,72 @@ namespace EndlessEscapade.Core.ECS; public abstract class ComponentStorage { - public abstract void Delete(int archetypeID); + /// + /// Moves the top component into , clearing the top spot if needed. + /// + public abstract void Delete(int index); + + /// + /// Returns and boxes the component at + /// + public abstract object? Box(int index); + + /// + /// Pulls a component from another component storage of the same type, and removes it from the other one. + /// This causes two copy operations and potentially one clear operation. + /// + public abstract void Pull(ComponentStorage other, int index, int capacity); } -public class ComponentStorage : ComponentStorage +public sealed class ComponentStorage : ComponentStorage { + public int Capacity => _items.Length; + + public override void Pull(ComponentStorage other, int otherIndex, int myCapacity, ) + { + var typedComponentStorage = (ComponentStorage)other; + this[capacity] = typedComponentStorage[index]; + typedComponentStorage.Delete(index); + } + + public override void Delete(int index, int capacity) + { + var local = _items; + if(!(uint)index < (uint)local.Length && (uint)capacity < (uint)local.Length) + ThrowHelper.Throw_ArgumentOutOfRange("Invalid Index", index); + + ref var top = ref local[capacity]; + ref var bottom = ref local[index]; + + bottom = top; + if(RuntimeHelpers.IsReferenceOrConatinsReferences()) + top = default; + } + + private T[] _items; + + public ComponentStorage() + { + _items = []; + } + public ref T this[int index] { - get => throw new NotImplementedException(); + get + { + var locItems = _items; + if ((uint)index < (uint)_items.Length) + { + return ref locItems[index]; + } + + return ref ResizeAndGet(index); + } + } + + private ref T ResizeAndGet(int index) + { + Array.Resize(ref _items, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + return ref _items[index]; } } \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/World.cs b/src/EndlessEscapade/Core/ECS/World.cs index 9fb44836..8141840c 100644 --- a/src/EndlessEscapade/Core/ECS/World.cs +++ b/src/EndlessEscapade/Core/ECS/World.cs @@ -46,11 +46,16 @@ public bool Delete(Entity entity) ref var location = ref Table[entity.EntityID]; if(location.Version != entity.EntityVersion) return false; - foreach(var storage in location.Archetype.Storages) - storage.Delete(location.Index); - throw new NotImplementedException(); - _recycledIDs.Push(new(entity.EntityID, entity.EntityVersion)); + + var deletedEntity = location.Archetype.Delete(); + + ref var movedDownEntityLocation = ref Table[deletedEntity.Index]; + movedDownEntityLocation = location; location = EntityLocation.Default; + + _recycledIDs.Push(new(entity.EntityID, entity.EntityVersion)); + + return true; } internal struct EntityLocation(Archetype archetype, int index, int version) From a188f024937e3881b4285eb6ce1ce3a5658742c6 Mon Sep 17 00:00:00 2001 From: itsBuggingMe Date: Mon, 10 Mar 2025 18:05:13 -0400 Subject: [PATCH 5/8] finish basic ecs impl --- .../Core/Collections/_Generic/FastStack.cs | 2 +- src/EndlessEscapade/Core/ECS/Archetype.cs | 90 --------- src/EndlessEscapade/Core/ECS/Component.cs | 18 -- src/EndlessEscapade/Core/ECS/ComponentID.cs | 12 -- .../Core/ECS/ComponentStorage.cs | 79 -------- .../Core/ECS/Data/Archetype.cs | 130 ++++++++++++ .../Core/ECS/{ => Data}/ArchetypeID.cs | 5 +- .../Core/ECS/Data/Component.cs | 33 +++ .../Core/ECS/Data/ComponentID.cs | 19 ++ .../Core/ECS/Data/ComponentStorage.cs | 114 +++++++++++ src/EndlessEscapade/Core/ECS/Entity.cs | 6 +- src/EndlessEscapade/Core/ECS/EntityLight.cs | 4 +- .../Core/ECS/EntityTemplate.cs | 40 ++-- src/EndlessEscapade/Core/ECS/Ref.cs | 6 +- src/EndlessEscapade/Core/ECS/Sig.cs | 189 ------------------ .../Core/ECS/Systems/ISystem.cs | 11 + src/EndlessEscapade/Core/ECS/Systems/Query.cs | 46 +++++ .../Core/ECS/Systems/QueryTemplate.cs | 29 +++ src/EndlessEscapade/Core/ECS/Systems/Rule.cs | 27 +++ .../Core/ECS/Systems/SystemGroup.cs | 26 +++ src/EndlessEscapade/Core/ECS/World.cs | 134 ++++++++++++- 21 files changed, 588 insertions(+), 432 deletions(-) delete mode 100644 src/EndlessEscapade/Core/ECS/Archetype.cs delete mode 100644 src/EndlessEscapade/Core/ECS/Component.cs delete mode 100644 src/EndlessEscapade/Core/ECS/ComponentID.cs delete mode 100644 src/EndlessEscapade/Core/ECS/ComponentStorage.cs create mode 100644 src/EndlessEscapade/Core/ECS/Data/Archetype.cs rename src/EndlessEscapade/Core/ECS/{ => Data}/ArchetypeID.cs (62%) create mode 100644 src/EndlessEscapade/Core/ECS/Data/Component.cs create mode 100644 src/EndlessEscapade/Core/ECS/Data/ComponentID.cs create mode 100644 src/EndlessEscapade/Core/ECS/Data/ComponentStorage.cs delete mode 100644 src/EndlessEscapade/Core/ECS/Sig.cs create mode 100644 src/EndlessEscapade/Core/ECS/Systems/ISystem.cs create mode 100644 src/EndlessEscapade/Core/ECS/Systems/Query.cs create mode 100644 src/EndlessEscapade/Core/ECS/Systems/QueryTemplate.cs create mode 100644 src/EndlessEscapade/Core/ECS/Systems/Rule.cs create mode 100644 src/EndlessEscapade/Core/ECS/Systems/SystemGroup.cs diff --git a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs index 606f5d6d..41470f00 100644 --- a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs +++ b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Security; -namespace EndlessEscapade.Core.Collections._Generic; +namespace EndlessEscapade.Core.Collections; /// /// This struct is meant to be used purely inside of classes as fields. diff --git a/src/EndlessEscapade/Core/ECS/Archetype.cs b/src/EndlessEscapade/Core/ECS/Archetype.cs deleted file mode 100644 index 1a993bcc..00000000 --- a/src/EndlessEscapade/Core/ECS/Archetype.cs +++ /dev/null @@ -1,90 +0,0 @@ -using EndlessEscapade.Core.Collections._Generic; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Data.Common; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Terraria; - -namespace EndlessEscapade.Core.ECS; - -public partial class Archetype -{ - private static int nextArchetypeID; - - private static FastStack<(ImmutableArray Components, byte[] IndexMap)> archetypeMetadata = new(); - private static readonly Dictionary<(ulong h1, ulong h2), ArchetypeID> ExistingArchetypeIDs = []; - - public static ArchetypeID GetArchetypeID(Span types) - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(types.Length, byte.MaxValue); - - ulong hash1 = 0; - ulong hash2 = 0; - foreach (var type in types) - { - hash1 ^= (ulong)type.GetHashCode() * 98317U; - hash2 += (ulong)type.GetHashCode() * 53U; - } - - ref ArchetypeID id = ref CollectionsMarshal.GetValueRefOrAddDefault(ExistingArchetypeIDs, (hash1, hash2), out bool exists); - if (exists) - return id; - - ushort newRawId = checked((ushort)nextArchetypeID); - - return id = ArchetypeID.CreateNew(newRawId); - } - - public static Archetype Create(ArchetypeID id) - { - ref var arr = ref archetypeMetadata[id.GetRawValue()]; - var components = arr.Components; - - ComponentStorage[] storages = new ComponentStorage[components.Length]; - for(int i = 0; i < components.Length; i++) - { - storages[i] = Component.Create(components[i]); - } - - return new Archetype(storages, arr.IndexMap); - } -} - -public partial class Archetype(ComponentStorage[] storages, byte[] indexMap) -{ - internal readonly ComponentStorage[] Storages = storages; - private readonly byte[] _indexMap = indexMap; - private readonly ComponentStorage _entities = new(); - private int _nextIndex; - public int Create() - { - throw new NotImplementedException(); - if(_nextIndex++ == _entities.Capacity) - { - - } - } - - /// - /// Moves the top entity's components into and clears the top slot if needed. - /// - /// The entity id and version of the entity that was moved down - public EntityLight Delete(int index) - { - foreach(var stor in Storages) - stor.Delete(index); - var @return = _entities[_entities.Capacity - 1]; - _entities.Delete(index); - return @return; - } - - public ref T GetComponent(int index) => ref ((ComponentStorage)Storages[_indexMap[Component.ID.GetRawValue()]])[index]; - public ref T GetComponentKnownComponentStorageIndex(int index, int storageIndex) => - ref ((ComponentStorage)Storages[storageIndex])[index]; - public bool HasComponent(out int index) => (index = _indexMap[Component.ID.GetRawValue()]) != -1; -} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Component.cs b/src/EndlessEscapade/Core/ECS/Component.cs deleted file mode 100644 index b78eba6e..00000000 --- a/src/EndlessEscapade/Core/ECS/Component.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EndlessEscapade.Core.ECS; -internal class Component -{ - public static readonly ComponentID ID = default; - public static ComponentStorage Create() => throw new NotImplementedException(); -} - -internal class Component -{ - public static ComponentStorage Create(ComponentID componentID) => throw new NotImplementedException(); -} - diff --git a/src/EndlessEscapade/Core/ECS/ComponentID.cs b/src/EndlessEscapade/Core/ECS/ComponentID.cs deleted file mode 100644 index 0f6bb11e..00000000 --- a/src/EndlessEscapade/Core/ECS/ComponentID.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EndlessEscapade.Core.ECS; - -internal struct ComponentID -{ - private ushort _value; -} diff --git a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs b/src/EndlessEscapade/Core/ECS/ComponentStorage.cs deleted file mode 100644 index 7c5538fb..00000000 --- a/src/EndlessEscapade/Core/ECS/ComponentStorage.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EndlessEscapade.Core.ECS; - -public abstract class ComponentStorage -{ - /// - /// Moves the top component into , clearing the top spot if needed. - /// - public abstract void Delete(int index); - - /// - /// Returns and boxes the component at - /// - public abstract object? Box(int index); - - /// - /// Pulls a component from another component storage of the same type, and removes it from the other one. - /// This causes two copy operations and potentially one clear operation. - /// - public abstract void Pull(ComponentStorage other, int index, int capacity); -} - -public sealed class ComponentStorage : ComponentStorage -{ - public int Capacity => _items.Length; - - public override void Pull(ComponentStorage other, int otherIndex, int myCapacity, ) - { - var typedComponentStorage = (ComponentStorage)other; - this[capacity] = typedComponentStorage[index]; - typedComponentStorage.Delete(index); - } - - public override void Delete(int index, int capacity) - { - var local = _items; - if(!(uint)index < (uint)local.Length && (uint)capacity < (uint)local.Length) - ThrowHelper.Throw_ArgumentOutOfRange("Invalid Index", index); - - ref var top = ref local[capacity]; - ref var bottom = ref local[index]; - - bottom = top; - if(RuntimeHelpers.IsReferenceOrConatinsReferences()) - top = default; - } - - private T[] _items; - - public ComponentStorage() - { - _items = []; - } - - public ref T this[int index] - { - get - { - var locItems = _items; - if ((uint)index < (uint)_items.Length) - { - return ref locItems[index]; - } - - return ref ResizeAndGet(index); - } - } - - private ref T ResizeAndGet(int index) - { - Array.Resize(ref _items, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); - return ref _items[index]; - } -} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Data/Archetype.cs b/src/EndlessEscapade/Core/ECS/Data/Archetype.cs new file mode 100644 index 00000000..52a530e6 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/Archetype.cs @@ -0,0 +1,130 @@ +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Terraria; + +namespace EndlessEscapade.Core.ECS; + +public partial class Archetype +{ + private static int nextArchetypeID; + + static Archetype() + { + _ = GetArchetypeID([]); + } + + internal static FastStack<(ImmutableArray Components, byte[] IndexMap)> MetadataTable = new(); + private static readonly Dictionary<(ulong h1, ulong h2), ArchetypeID> ExistingArchetypeIDs = []; + + public static ArchetypeID GetArchetypeID(ReadOnlySpan types) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(types.Length, byte.MaxValue); + + ulong hash1 = 0; + ulong hash2 = 0; + foreach (var type in types) + { + hash1 ^= (ulong)type.GetHashCode() * 98317U; + hash2 += (ulong)type.GetHashCode() * 53U; + } + + ref ArchetypeID id = ref CollectionsMarshal.GetValueRefOrAddDefault(ExistingArchetypeIDs, (hash1, hash2), out bool exists); + if (exists) + return id; + + ushort newRawId = checked((ushort)nextArchetypeID++); + + int max = int.MinValue; + foreach(var type in types) + max = Math.Max(max, type.GetRawValue()); + + byte[] indexMap = new byte[max]; + indexMap.AsSpan().Fill(byte.MaxValue); + + for(int i = 0; i < types.Length; i++) + { + indexMap[types[i].GetRawValue()] = (byte)i; + } + + MetadataTable.Push((types.ToImmutableArray(), indexMap)); + + return id = ArchetypeID.CreateNew(newRawId); + } + + public static Archetype Create(ArchetypeID id) + { + ref var arr = ref MetadataTable[id.GetRawValue()]; + var components = arr.Components; + + ComponentStorage[] storages = new ComponentStorage[components.Length]; + for(int i = 0; i < components.Length; i++) + { + storages[i] = Component.Create(components[i]); + } + + return new Archetype(id, storages, arr.IndexMap); + } +} + +public partial class Archetype(ArchetypeID id, ComponentStorage[] storages, byte[] indexMap) +{ + internal readonly ArchetypeID ID = id; + internal readonly ComponentStorage[] Storages = storages; + internal readonly byte[] IndexMap = indexMap; + private readonly ComponentStorage _entities = new(); + private int _nextIndex; + public int Create(out Ref slot) + { + if (_nextIndex == _entities.Capacity) + Resize(_nextIndex << 1); + slot = new(ref _entities[_nextIndex]); + return _nextIndex++; + } + + private void Resize(int newSize) + { + foreach (var i in Storages) + i.Resize(newSize); + _entities.Resize(newSize); + } + + /// + /// Moves the top entity's components into and clears the top slot if needed. + /// + /// The entity id and version of the entity that was moved down + public EntityLight Delete(int index) + { + int capacity = _entities.Capacity; + foreach(var stor in Storages) + stor.Delete(index, capacity); + var @return = _entities[capacity - 1]; + _entities.Delete(index, capacity); + return @return; + } + + public EntityLight DeleteFromEntityStorageOnly(int index) + { + int capacity = _entities.Capacity; + var @return = _entities[capacity - 1]; + _entities.Delete(index, capacity); + return @return; + } + + public ref T GetComponent(int index) => ref ((ComponentStorage)Storages[IndexMap[Component.ID.GetRawValue()]])[index]; + public ref T GetComponentKnownComponentStorageIndex(int index, int storageIndex) => + ref ((ComponentStorage)Storages[storageIndex])[index]; + public bool HasComponent(out int index) => (index = IndexMap[Component.ID.GetRawValue()]) != -1; + public ComponentStorage GetComponentStorage() => (ComponentStorage)Storages[IndexMap[Component.ID.GetRawValue()]]; + public int Capacity => _entities.Capacity; + public int Count => _nextIndex; +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/ArchetypeID.cs b/src/EndlessEscapade/Core/ECS/Data/ArchetypeID.cs similarity index 62% rename from src/EndlessEscapade/Core/ECS/ArchetypeID.cs rename to src/EndlessEscapade/Core/ECS/Data/ArchetypeID.cs index 0e0976e5..3fd60bce 100644 --- a/src/EndlessEscapade/Core/ECS/ArchetypeID.cs +++ b/src/EndlessEscapade/Core/ECS/Data/ArchetypeID.cs @@ -1,16 +1,17 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EndlessEscapade.Core.ECS; +namespace EndlessEscapade.Core.ECS.Data; public struct ArchetypeID { private ushort _value; - + public readonly ImmutableArray ComponentIDs => Archetype.MetadataTable[_value].Components; internal ushort GetRawValue() => _value; diff --git a/src/EndlessEscapade/Core/ECS/Data/Component.cs b/src/EndlessEscapade/Core/ECS/Data/Component.cs new file mode 100644 index 00000000..b3a268ed --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/Component.cs @@ -0,0 +1,33 @@ +using EndlessEscapade.Core.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace EndlessEscapade.Core.ECS.Data; +internal class Component +{ + public static readonly ComponentID ID = Component.GetComponentID(); +} + +internal static class Component +{ + private static int nextComponentID = 1; + private static Dictionary existingComponentIDs = []; + internal static FastStack<(Type UnderlyingType, Func StorageFactory)> ComponentMetaDataTable = new(); + public static ComponentStorage Create(ComponentID componentID) => ComponentMetaDataTable[componentID.GetRawValue()].StorageFactory(); + + static Component() + { + ComponentMetaDataTable.Push((typeof(void), () => throw new NotSupportedException("Cannot create component storage of default(ComponentID)"))); + } + + public static ComponentID GetComponentID() + { + ref ComponentID elem = ref CollectionsMarshal.GetValueRefOrAddDefault(existingComponentIDs, typeof(T), out var exists); + if (exists) + return elem; + + ComponentMetaDataTable.Push((typeof(T), () => new ComponentStorage())); + + return elem = ComponentID.CreateFromRawValue((ushort)nextComponentID++); + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Data/ComponentID.cs b/src/EndlessEscapade/Core/ECS/Data/ComponentID.cs new file mode 100644 index 00000000..5bac6d79 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/ComponentID.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS.Data; + +public struct ComponentID : IEquatable +{ + private ushort _value; + public Type Type => Component.ComponentMetaDataTable[_value].UnderlyingType; + public bool Equals(ComponentID other) => other._value == _value; + public static bool operator ==(ComponentID left, ComponentID right) => left.Equals(right); + public static bool operator !=(ComponentID left, ComponentID right) => !left.Equals(right); + public ushort GetRawValue() => _value; + public static ComponentID CreateFromRawValue(ushort raw) => new() { _value = raw }; +} diff --git a/src/EndlessEscapade/Core/ECS/Data/ComponentStorage.cs b/src/EndlessEscapade/Core/ECS/Data/ComponentStorage.cs new file mode 100644 index 00000000..c43fac95 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/ComponentStorage.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS.Data; + +public abstract class ComponentStorage +{ + /// + /// Moves the top component into , clearing the top spot if needed. + /// + public abstract void Delete(int index, int capacity); + + /// + /// Returns and boxes the component at + /// + public abstract object? Box(int index); + + /// + /// Pulls a component from another component storage of the same type, and removes it from the other one. + /// This causes two copy operations and potentially one clear operation. + /// + public abstract void Pull(Archetype other, int otherIndex, int myIndex, int otherTop); + + public abstract void Resize(int size); +} + +public sealed class ComponentStorage : ComponentStorage +{ + public int Capacity => _items.Length; + + public override void Resize(int size) => Array.Resize(ref _items, size); + + public override void Pull(Archetype other, int otherIndex, int myIndex, int otherTop) + { + var typedComponentStorage = other.GetComponentStorage(); + + // key + // x == item, - == empty + + // this | other + // x | x + // x | x <- otherIndex/center + // x | x + // - <- myIndex| x <- otherTop + // - | - + + //center -> myIndex + //otherIndex -> center + //clear otherIndex + + var localOther = typedComponentStorage._items; + if (!((uint)otherIndex < (uint)localOther.Length && (uint)otherTop < (uint)localOther.Length)) + ThrowHelper.Throw_ArgumentOutOfRange("Invalid Index", otherIndex); + + ref var center = ref localOther[otherIndex]; + + this[myIndex] = center; + + + ref var top = ref localOther[otherTop]; + center = top; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + top = default; + } + + public override void Delete(int index, int capacity) + { + var local = _items; + if (!((uint)index < (uint)local.Length && (uint)capacity < (uint)local.Length)) + ThrowHelper.Throw_ArgumentOutOfRange("Invalid Index", index); + + ref var top = ref local[capacity]; + ref var bottom = ref local[index]; + + bottom = top; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + top = default; + } + + private T[] _items; + + public ComponentStorage() + { + _items = []; + } + + public ref T this[int index] + { + get + { + var locItems = _items; + if ((uint)index < (uint)locItems.Length) + { + return ref locItems[index]; + } + + return ref ResizeAndGet(index); + } + } + + private ref T ResizeAndGet(int index) + { + Array.Resize(ref _items, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + return ref _items[index]; + } + + public override object? Box(int index) => _items[index]; +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Entity.cs b/src/EndlessEscapade/Core/ECS/Entity.cs index bfae014d..a6c471b0 100644 --- a/src/EndlessEscapade/Core/ECS/Entity.cs +++ b/src/EndlessEscapade/Core/ECS/Entity.cs @@ -29,9 +29,11 @@ public readonly bool TryGet(out Ref item) if(!location.Archetype.HasComponent(out int storageIndex)) goto noComponent; - return ref location.Archetype.GetComponentKnownComponentStorageIndex( + item = new(ref location.Archetype.GetComponentKnownComponentStorageIndex( location.Index, - storageIndex); + storageIndex)); + + return true; noComponent: item = default!; diff --git a/src/EndlessEscapade/Core/ECS/EntityLight.cs b/src/EndlessEscapade/Core/ECS/EntityLight.cs index 1896baf4..8997f973 100644 --- a/src/EndlessEscapade/Core/ECS/EntityLight.cs +++ b/src/EndlessEscapade/Core/ECS/EntityLight.cs @@ -1,2 +1,2 @@ -namespace EndlessEscapade.Core.ECS; -public record struct EntityLight(int Index, int Version); \ No newline at end of file +namespace EndlessEscapade.Core.ECS; +public record struct EntityLight(int ID, int Version); \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/EntityTemplate.cs b/src/EndlessEscapade/Core/ECS/EntityTemplate.cs index 4f99f467..03f48e12 100644 --- a/src/EndlessEscapade/Core/ECS/EntityTemplate.cs +++ b/src/EndlessEscapade/Core/ECS/EntityTemplate.cs @@ -3,14 +3,15 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using EndlessEscapade.Core.Collections._Generic; +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; namespace EndlessEscapade.Core.ECS; public struct Add : IEntityTemplate where TRest : IEntityTemplate { - public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID>(); + public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID>(); private static ArchetypeID? cache; @@ -28,9 +29,9 @@ public struct Add : IEntityTemplate Value = value }; - public void AppendTypes(ref FastStack types) + public void AppendTypes(ref FastStack types) { - types.Push(typeof(TRest)); + types.Push(Component.ID); Rest.AppendTypes(ref types); } @@ -41,42 +42,27 @@ public void SetComponents(Archetype archetype, int index) } } -public struct EntityTemplate : IEntityTemplate +public struct EntityTemplate : IEntityTemplate { - public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID>(); + public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID(); private static ArchetypeID? cache; public required World World { get; init; } public Entity Entity => World.SetComponents(ref this, ArchetypeID); - - public T Value; - - - public Add, TN> Set(TN value) => new() + public Add Set(TN value) => new() { Rest = this, Value = value, }; - public void AppendTypes(ref FastStack types) => types.Push(typeof(T)); - - public void SetComponents(Archetype archetype, int index) => archetype.GetComponent(index) = Value; -} - -public struct EntityTemplate -{ - public World World; - public EntityTemplate Set(T value) => new() - { - World = World, - Value = value, - }; + public void AppendTypes(ref FastStack types) { } + public void SetComponents(Archetype archetype, int index) { } } public interface IEntityTemplate { - public void AppendTypes(ref FastStack types); + public void AppendTypes(ref FastStack types); public void SetComponents(Archetype archetype, int index); public Entity Entity { get; } public ArchetypeID ArchetypeID { get; } @@ -85,7 +71,7 @@ public interface IEntityTemplate static ArchetypeID CalculateArchetypeID() where T : struct, IEntityTemplate { - ref FastStack types = ref sharedStack; + ref FastStack types = ref sharedStack; default(T)!.AppendTypes(ref types); var id = Archetype.GetArchetypeID(types.AsSpan()); types.Clear(); @@ -93,5 +79,5 @@ static ArchetypeID CalculateArchetypeID() } [ThreadStatic] - private static FastStack sharedStack = new(); + private static FastStack sharedStack = new(); } \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Ref.cs b/src/EndlessEscapade/Core/ECS/Ref.cs index e2f0593f..6ce7c3a7 100644 --- a/src/EndlessEscapade/Core/ECS/Ref.cs +++ b/src/EndlessEscapade/Core/ECS/Ref.cs @@ -1,4 +1,4 @@ -using EndlessEscapade.Core.Collections._Generic; +using EndlessEscapade.Core.Collections; using EndlessEscapade.Framework.Collections; using System; using System.Collections.Generic; @@ -8,7 +8,7 @@ namespace EndlessEscapade.Core.ECS; -public ref struct Ref +public ref struct Ref(ref T r) { - public ref T Ref; + public ref T R = ref r; } \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Sig.cs b/src/EndlessEscapade/Core/ECS/Sig.cs deleted file mode 100644 index 0fde2ff4..00000000 --- a/src/EndlessEscapade/Core/ECS/Sig.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics; - -namespace EndlessEscapade.Core.ECS; -//todo: agg inline all -public struct Rec : IRec -{ - private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); - public readonly ArchetypeID ArchetypeID => CachedArchetypeID; - - public T Item1; - - public T1 GetAt(int index) - { - if (index != 0) - throw new IndexOutOfRangeException(); - return (T1)(object)Item1!; - } - - public void AppendTypes(List appendTo) => appendTo.Add(typeof(T)); - public void SetArchetype(Archetype archetype, int index) => archetype.GetComponent(index) = Item1; - - public static implicit operator Rec(T val) => new() { Item1 = val }; -} - -public struct Rec : IRec -{ - private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); - public readonly ArchetypeID ArchetypeID => CachedArchetypeID; - - public T1 Item1; - public T2 Item2; - - public T GetAt(int index) - { - return index switch - { - 0 => (T)(object)Item1!, - 1 => (T)(object)Item2!, - _ => throw new IndexOutOfRangeException() - }; - } - - public void AppendTypes(List appendTo) - { - appendTo.Add(typeof(T1)); - appendTo.Add(typeof(T2)); - } - - public void SetArchetype(Archetype archetype, int index) - { - archetype.GetComponent(index) = Item1; - archetype.GetComponent(index) = Item2; - } - - public static implicit operator Rec((T1, T2) val) => new() { Item1 = val.Item1, Item2 = val.Item2 }; -} - -public struct Rec : IRec -{ - private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); - public readonly ArchetypeID ArchetypeID => CachedArchetypeID; - - public T1 Item1; - public T2 Item2; - public T3 Item3; - - public T GetAt(int index) - { - return index switch - { - 0 => (T)(object)Item1!, - 1 => (T)(object)Item2!, - 2 => (T)(object)Item3!, - _ => throw new IndexOutOfRangeException() - }; - } - - public void AppendTypes(List appendTo) - { - appendTo.Add(typeof(T1)); - appendTo.Add(typeof(T2)); - appendTo.Add(typeof(T3)); - } - - public void SetArchetype(Archetype archetype, int index) - { - archetype.GetComponent(index) = Item1; - archetype.GetComponent(index) = Item2; - archetype.GetComponent(index) = Item3; - } - - public static implicit operator Rec((T1, T2, T3) val) => new() { Item1 = val.Item1, Item2 = val.Item2, Item3 = val.Item3 }; -} - -public struct Rec : IRec -{ - private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); - public readonly ArchetypeID ArchetypeID => CachedArchetypeID; - - public T1 Item1; - public T2 Item2; - public T3 Item3; - public T4 Item4; - - public T GetAt(int index) - { - return index switch - { - 0 => (T)(object)Item1!, - 1 => (T)(object)Item2!, - 2 => (T)(object)Item3!, - 3 => (T)(object)Item4!, - _ => throw new IndexOutOfRangeException() - }; - } - - public void AppendTypes(List appendTo) - { - appendTo.Add(typeof(T1)); - appendTo.Add(typeof(T2)); - appendTo.Add(typeof(T3)); - appendTo.Add(typeof(T4)); - } - - public void SetArchetype(Archetype archetype, int index) - { - archetype.GetComponent(index) = Item1; - archetype.GetComponent(index) = Item2; - archetype.GetComponent(index) = Item3; - archetype.GetComponent(index) = Item4; - } - - public static implicit operator Rec((T1, T2, T3, T4) val) => new() { Item1 = val.Item1, Item2 = val.Item2, Item3 = val.Item3, Item4 = val.Item4 }; -} - -public struct Rec : IRec where TRest : IRec -{ - private static readonly ArchetypeID CachedArchetypeID = Archetype.GetArchetypeID>(); - public readonly ArchetypeID ArchetypeID => CachedArchetypeID; - - public T1 Item1; - public T2 Item2; - public T3 Item3; - public T4 Item4; - public TRest Rest; - - public T GetAt(int index) - { - return index switch - { - 0 => (T)(object)Item1!, - 1 => (T)(object)Item2!, - 2 => (T)(object)Item3!, - 3 => (T)(object)Item4!, - _ => Rest.GetAt(index - 4) - }; - } - - public void AppendTypes(List appendTo) - { - appendTo.Add(typeof(T1)); - appendTo.Add(typeof(T2)); - appendTo.Add(typeof(T3)); - appendTo.Add(typeof(T4)); - Rest.AppendTypes(appendTo); - } - - public void SetArchetype(Archetype archetype, int index) - { - archetype.GetComponent(index) = Item1; - archetype.GetComponent(index) = Item2; - archetype.GetComponent(index) = Item3; - archetype.GetComponent(index) = Item4; - - Rest.SetArchetype(archetype, index); - } -} - - -public interface IRec -{ - ArchetypeID ArchetypeID { get; } - public T GetAt(int index); - public void AppendTypes(List appendTo); - public void SetArchetype(Archetype archetype, int index); - [ThreadStatic] - public static readonly List SharedTypeList = []; -} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Systems/ISystem.cs b/src/EndlessEscapade/Core/ECS/Systems/ISystem.cs new file mode 100644 index 00000000..fcad818a --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/ISystem.cs @@ -0,0 +1,11 @@ +namespace EndlessEscapade.Core.ECS.Systems; + +internal interface ISystem +{ + /// + /// Lower == executed earlier + /// + int Order => 0; + Query BuildQuery(World world); + public void Execute(Query query); +} diff --git a/src/EndlessEscapade/Core/ECS/Systems/Query.cs b/src/EndlessEscapade/Core/ECS/Systems/Query.cs new file mode 100644 index 00000000..7f5d62aa --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/Query.cs @@ -0,0 +1,46 @@ +using EndlessEscapade.Core.Collections; +using System.Collections.Immutable; + +namespace EndlessEscapade.Core.ECS.Systems; + +public class Query +{ + public World World { get; init; } + private FastStack _appliesTo; + private ImmutableArray _rules; + + public Query(World world, ImmutableArray rules) + { + World = world; + world.OnArchetypeAdded += a => + { + if(Applies(a, _rules.AsSpan())) + _appliesTo.Push(a); + }; + + _rules = rules; + + foreach(var archetype in world.Archetypes) + { + if(Applies(archetype, rules.AsSpan())) + { + _appliesTo.Push(archetype); + } + } + } + + public ReadOnlySpan Archetypes => _appliesTo.AsSpan(); + + private static bool Applies(Archetype archetype, ReadOnlySpan rules) + { + var indicies = archetype.IndexMap; + foreach(var item in rules) + { + if(!item.Applies(indicies)) + { + return false; + } + } + return true; + } +} diff --git a/src/EndlessEscapade/Core/ECS/Systems/QueryTemplate.cs b/src/EndlessEscapade/Core/ECS/Systems/QueryTemplate.cs new file mode 100644 index 00000000..b45ac6db --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/QueryTemplate.cs @@ -0,0 +1,29 @@ +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; +using System.Collections.Immutable; + +namespace EndlessEscapade.Core.ECS.Systems; + +public struct QueryBuilder(World world) +{ + public World World { get; } = world; + + private FastStack _rules = new(); + + public QueryBuilder With() + { + _rules.Push(new Rule(Rule.RuleType.Include, Component.ID)); + return this; + } + + public QueryBuilder Without() + { + _rules.Push(new Rule(Rule.RuleType.Exclude, Component.ID)); + return this; + } + + public Query Build() + { + return new Query(World, _rules.AsSpan().ToImmutableArray()); + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Systems/Rule.cs b/src/EndlessEscapade/Core/ECS/Systems/Rule.cs new file mode 100644 index 00000000..21d9ef78 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/Rule.cs @@ -0,0 +1,27 @@ +using EndlessEscapade.Core.ECS.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS.Systems; +public readonly struct Rule(Rule.RuleType type, ComponentID operand) +{ + public readonly RuleType Operator = type; + public readonly ComponentID Operand = operand; + + public bool Applies(byte[] indexMapping) => + Operator switch + { + RuleType.Include => indexMapping[Operand.GetRawValue()] != byte.MaxValue, + RuleType.Exclude => indexMapping[Operand.GetRawValue()] == byte.MaxValue, + _ => throw new NotSupportedException(), + }; + + public enum RuleType + { + Include, + Exclude, + } +} diff --git a/src/EndlessEscapade/Core/ECS/Systems/SystemGroup.cs b/src/EndlessEscapade/Core/ECS/Systems/SystemGroup.cs new file mode 100644 index 00000000..e8c95edb --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/SystemGroup.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EndlessEscapade.Core.ECS.Systems; +internal class SystemGroup +{ + private SortedList _systems; + private World _world; + + public SystemGroup(World world, params ISystem[] systems) + { + _world = world; + _systems = new(systems.Length); + foreach(var item in systems) + _systems.Add(item.Order, (item, item.BuildQuery(world))); + } + + public void Add(ISystem system) => _systems.Add(system.Order, (system, system.BuildQuery(_world))); + + public void Execute() + { + foreach ((_, (var system, var query)) in _systems) + { + system.Execute(query); + } + } +} diff --git a/src/EndlessEscapade/Core/ECS/World.cs b/src/EndlessEscapade/Core/ECS/World.cs index 8141840c..9c0ef171 100644 --- a/src/EndlessEscapade/Core/ECS/World.cs +++ b/src/EndlessEscapade/Core/ECS/World.cs @@ -1,22 +1,31 @@ -using EndlessEscapade.Core.Collections._Generic; +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; +using EndlessEscapade.Core.ECS.Systems; using EndlessEscapade.Framework.Collections; +using Microsoft.CodeAnalysis; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; using System.Threading.Tasks; +using Terraria; namespace EndlessEscapade.Core.ECS; public class World { + public Action? OnArchetypeAdded; + //archetypeID -> Archetype private SparseSet _archetypes = new(); - private Dictionary<(bool IsAddAction, Type Delta, Archetype Archetype), Archetype> _archetypeGraph = []; + private Dictionary _archetypeGraph = []; private Dictionary, HashSet> QueryCache = []; internal DenseSet Table = new(); @@ -26,11 +35,13 @@ public class World public Entity SetComponents(ref readonly T template, ArchetypeID archetypeID) where T : IEntityTemplate { - (int id, int version) = _recycledIDs.TryPop(out var newId) ? newId : (_nextID++, 1); + (int id, int version) = _recycledIDs.TryPop(out var newId) ? newId : new(_nextID++, 1); - var archetype = _archetypes[archetypeID.GetRawValue()] ??= Archetype.Create(archetypeID); + var archetype = _archetypes[archetypeID.GetRawValue()] ??= InvokeArchetypeEvents(Archetype.Create(archetypeID)); - var index = archetype.Create(); + var index = archetype.Create(out var entitySlot); + entitySlot.R.ID = id; + entitySlot.R.Version = id; Table[id] = new EntityLocation(archetype, index, version); @@ -41,15 +52,118 @@ public Entity SetComponents(ref readonly T template, ArchetypeID archetypeID) public EntityTemplate Entity() => new() { World = this }; + public bool Add(Entity entity, in T component) + { + Debug.Assert(entity.World == this); + + ref EntityLocation location = ref Table[entity.EntityID]; + if (location.Archetype.HasComponent(out _) || location.Version != entity.EntityVersion) + return false; + + var destination = CollectionsMarshal.GetValueRefOrAddDefault(_archetypeGraph, new ArchetypeGraphEdge(true, Component.ID, location.Archetype), out _) + ??= FindAdjacientArchetypeCold(true, location.Archetype, Component.ID); + + MoveEntityTo(entity, ref location, location.Archetype, destination); + + destination.GetComponent(location.Index) = component; + + var world = this; + + return true; + } + + public bool Remove(Entity entity) + { + Debug.Assert(entity.World == this); + + ref EntityLocation location = ref Table[entity.EntityID]; + if (!location.Archetype.HasComponent(out _) || location.Version != entity.EntityVersion) + return false; + + var destination = CollectionsMarshal.GetValueRefOrAddDefault(_archetypeGraph, new ArchetypeGraphEdge(false, Component.ID, location.Archetype), out _) + ??= FindAdjacientArchetypeCold(false, location.Archetype, Component.ID); + + location.Archetype.GetComponentStorage().Delete(location.Index, location.Archetype.Capacity); + MoveEntityTo(entity, ref location, location.Archetype, destination); + + + return true; + } + + private Archetype FindAdjacientArchetypeCold(bool isAdding, Archetype from, ComponentID type) + { + var types = from.ID.ComponentIDs.AsSpan(); + + ArchetypeID nextId; + + if (isAdding) + { + Span newIds = stackalloc ComponentID[types.Length + 1]; + types.CopyTo(newIds[..^1]); + newIds[^1] = type; + nextId = Archetype.GetArchetypeID(newIds); + } + else + { + Span newIds = stackalloc ComponentID[types.Length - 1]; + int index = 0; + foreach (var id in types) + if (id != type) + newIds[index++] = id; + Debug.Assert(index == newIds.Length); + nextId = Archetype.GetArchetypeID(newIds); + } + + return _archetypes[nextId.GetRawValue()] ??= InvokeArchetypeEvents(Archetype.Create(nextId)); + } + + private Archetype InvokeArchetypeEvents(Archetype archetype) + { + OnArchetypeAdded?.Invoke(archetype); + return archetype; + } + + /// + /// Moves the components of an entity from to . + /// If a component type is not in , it is not updated and needs to be done manually. + /// If a component type is not in , the destination skips it. + /// + private void MoveEntityTo(Entity entity, ref EntityLocation location, Archetype from, Archetype destination) + { + var fromIndex = location.Index; + var fromTop = location.Archetype.Count; + var destIndex = destination.Create(out var slot); + + foreach (var storage in destination.Storages) + { + storage.Pull(location.Archetype, fromIndex, destIndex, fromTop); + } + + //update entity records + + var movedEntity = from.DeleteFromEntityStorageOnly(fromIndex); + + ref var movedEntityLocation = ref Table[movedEntity.ID]; + + movedEntityLocation = location; + location.Archetype = destination; + location.Index = destIndex; + + slot.R.ID = entity.EntityID; + slot.R.Version = entity.EntityVersion; + + + } + public bool Delete(Entity entity) { ref var location = ref Table[entity.EntityID]; if(location.Version != entity.EntityVersion) return false; - var deletedEntity = location.Archetype.Delete(); + var deletedEntity = location.Archetype.Delete(location.Index); - ref var movedDownEntityLocation = ref Table[deletedEntity.Index]; + ref var movedDownEntityLocation = ref Table[deletedEntity.ID]; movedDownEntityLocation = location; location = EntityLocation.Default; @@ -58,6 +172,8 @@ public bool Delete(Entity entity) return true; } + public QueryBuilder Query() => new(this); + internal struct EntityLocation(Archetype archetype, int index, int version) { internal Archetype Archetype = archetype; @@ -66,4 +182,8 @@ internal struct EntityLocation(Archetype archetype, int index, int version) public static EntityLocation Default = new EntityLocation(null!, -1, -1); } + + public ReadOnlySpan Archetypes => _archetypes.AsSpan(); + + internal record struct ArchetypeGraphEdge(bool IsAddAction, ComponentID Delta, Archetype Archetype); } From 21268b8dcafd106993f0cebe01637fcf1d523dee Mon Sep 17 00:00:00 2001 From: itsBuggingMe Date: Mon, 10 Mar 2025 18:21:24 -0400 Subject: [PATCH 6/8] added methods to entity --- src/EndlessEscapade/Core/ECS/Entity.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EndlessEscapade/Core/ECS/Entity.cs b/src/EndlessEscapade/Core/ECS/Entity.cs index a6c471b0..1c0caf82 100644 --- a/src/EndlessEscapade/Core/ECS/Entity.cs +++ b/src/EndlessEscapade/Core/ECS/Entity.cs @@ -39,7 +39,8 @@ public readonly bool TryGet(out Ref item) item = default!; return false; } - + public bool Add(in T component) => World.Add(this, component); + public bool Remove() => World.Remove(this); public bool Delete() => World.Delete(this); private const string EntityIsDead = "Entity is Dead"; From c0f0195244dd05668a6ee2cafbbdea86e37172ba Mon Sep 17 00:00:00 2001 From: itsBuggingMe Date: Mon, 10 Mar 2025 18:34:46 -0400 Subject: [PATCH 7/8] fixed has component bug --- src/EndlessEscapade/Core/ECS/Data/Archetype.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EndlessEscapade/Core/ECS/Data/Archetype.cs b/src/EndlessEscapade/Core/ECS/Data/Archetype.cs index 52a530e6..c3ed2170 100644 --- a/src/EndlessEscapade/Core/ECS/Data/Archetype.cs +++ b/src/EndlessEscapade/Core/ECS/Data/Archetype.cs @@ -123,7 +123,7 @@ public EntityLight DeleteFromEntityStorageOnly(int index) public ref T GetComponent(int index) => ref ((ComponentStorage)Storages[IndexMap[Component.ID.GetRawValue()]])[index]; public ref T GetComponentKnownComponentStorageIndex(int index, int storageIndex) => ref ((ComponentStorage)Storages[storageIndex])[index]; - public bool HasComponent(out int index) => (index = IndexMap[Component.ID.GetRawValue()]) != -1; + public bool HasComponent(out int index) => (index = IndexMap[Component.ID.GetRawValue()]) != byte.MaxValue; public ComponentStorage GetComponentStorage() => (ComponentStorage)Storages[IndexMap[Component.ID.GetRawValue()]]; public int Capacity => _entities.Capacity; public int Count => _nextIndex; From 594a2c4bca37464ccc7bf8cd8320b04ff60035b2 Mon Sep 17 00:00:00 2001 From: Paperclip Date: Tue, 11 Mar 2025 10:52:55 -0400 Subject: [PATCH 8/8] fixed FastStack no early return --- src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs index 41470f00..10a97a74 100644 --- a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs +++ b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs @@ -50,6 +50,7 @@ public void Push(T item) if (_nextIndex < _buffer.Length) { buffer[_nextIndex++] = item; + return; } ResizeAndPush(item); @@ -133,4 +134,4 @@ public readonly void Dispose() { } public void Reset() => _currentIndex = -1; } #endregion Enumerable -} \ No newline at end of file +}