diff --git a/src/Collections/NativeArray.cs b/src/Collections/NativeArray.cs index eb897a6..8ceba1d 100644 --- a/src/Collections/NativeArray.cs +++ b/src/Collections/NativeArray.cs @@ -7,7 +7,7 @@ namespace MoonTools.ECS.Collections; public unsafe class NativeArray : IDisposable where T : unmanaged { private T* Elements; - public int Count { get; private set;} + public int Count { get; private set; } private int Capacity; private int ElementSize; @@ -61,7 +61,7 @@ public unsafe class NativeArray : IDisposable where T : unmanaged Count = 0; } - private void ResizeTo(int size) + public void ResizeTo(int size) { Capacity = size; Elements = (T*) NativeMemory.Realloc((void*) Elements, (nuint) (ElementSize * Capacity)); diff --git a/src/Collections/NativeArrayUntyped.cs b/src/Collections/NativeArrayUntyped.cs index 070effd..8ed392b 100644 --- a/src/Collections/NativeArrayUntyped.cs +++ b/src/Collections/NativeArrayUntyped.cs @@ -7,8 +7,8 @@ namespace MoonTools.ECS.Collections; internal unsafe class NativeArray : IDisposable { private nint Elements; - public int Count { get; private set;} - private int Capacity; + public int Count { get; private set; } + public int Capacity { get; private set; } public readonly int ElementSize; private bool IsDisposed; @@ -44,7 +44,7 @@ internal unsafe class NativeArray : IDisposable Elements = (nint) NativeMemory.Realloc((void*) Elements, (nuint) (ElementSize * Capacity)); } - private void ResizeTo(int capacity) + public void ResizeTo(int capacity) { Capacity = capacity; Elements = (nint) NativeMemory.Realloc((void*) Elements, (nuint) (ElementSize * Capacity)); @@ -65,6 +65,11 @@ internal unsafe class NativeArray : IDisposable Count -= 1; } + public void RemoveLastElement() + { + Count -= 1; + } + public void Append(T component) where T : unmanaged { if (Count >= Capacity) diff --git a/src/ComponentStorage.cs b/src/ComponentStorage.cs index 6edc876..5407282 100644 --- a/src/ComponentStorage.cs +++ b/src/ComponentStorage.cs @@ -1,89 +1,97 @@ -using System; -using System.Collections.Generic; +using System; using MoonTools.ECS.Collections; namespace MoonTools.ECS; -internal class ComponentStorage : IDisposable +public class ComponentStorage : IDisposable { - internal readonly Dictionary EntityIDToStorageIndex = new Dictionary(16); - internal readonly NativeArray Components; - internal readonly NativeArray EntityIDs; - internal readonly TypeId TypeId; - internal readonly int ElementSize; - + internal readonly NativeArray DenseArray = new NativeArray(); + internal readonly NativeArray SparseArray = new NativeArray(); + internal readonly NativeArray ElementArray; private bool IsDisposed; - public ComponentStorage(TypeId typeId, int elementSize) + public int ElementSize { get; private set; } + public int Count => DenseArray.Count; + + public ComponentStorage(int elementSize) { + for (var i = 0; i < 16; i += 1) + { + SparseArray.Append(Entity.Null); // sentinel value + } + + ElementArray = new NativeArray(elementSize); ElementSize = elementSize; - Components = new NativeArray(elementSize); - EntityIDs = new NativeArray(); - TypeId = typeId; } - public bool Any() - { - return Components.Count > 0; - } + public bool Any() => DenseArray.Count > 0; - public bool Has(Entity entity) + public ref T Get(Entity entity) where T : unmanaged { - return EntityIDToStorageIndex.ContainsKey(entity); - } - - public ref T Get(in Entity entity) where T : unmanaged - { - return ref Components.Get(EntityIDToStorageIndex[entity]); + if (entity.ID >= ElementArray.Capacity) + { + throw new Exception("oh noes"); + } + return ref ElementArray.Get(entity.ID); } public ref T GetFirst() where T : unmanaged { #if DEBUG - if (Components.Count == 0) + if (DenseArray.Count == 0) { throw new IndexOutOfRangeException("Component storage is empty!"); } #endif - return ref Components.Get(0); + return ref ElementArray.Get(DenseArray[0].ID); } - // Returns true if the entity had this component. - public bool Set(in Entity entity, in T component) where T : unmanaged + public bool Set(Entity entity, T component) where T : unmanaged { - if (EntityIDToStorageIndex.TryGetValue(entity, out var index)) + var newEntity = SparseArray[entity.ID] == Entity.Null; + if (newEntity) { - Components.Set(index, component); - return true; - } - else - { - EntityIDToStorageIndex[entity] = Components.Count; - EntityIDs.Append(entity); - Components.Append(component); - return false; - } - } - - // Returns true if the entity had this component. - public bool Remove(in Entity entity) - { - if (EntityIDToStorageIndex.TryGetValue(entity, out int index)) - { - var lastElementIndex = Components.Count - 1; - - var lastEntity = EntityIDs[lastElementIndex]; - - // move a component into the hole to maintain contiguous memory - Components.Delete(index); - EntityIDs.Delete(index); - EntityIDToStorageIndex.Remove(entity); - - // update the index if it changed - if (lastElementIndex != index) + // the entity is being added! let's do some record keeping + var index = DenseArray.Count; + DenseArray.Append(entity); + if (entity.ID >= SparseArray.Count) { - EntityIDToStorageIndex[lastEntity] = index; + var oldCount = SparseArray.Count; + SparseArray.ResizeTo(entity.ID + 1); + for (var i = oldCount; i < SparseArray.Count; i += 1) + { + SparseArray.Append(Entity.Null); // sentinel value + } } + SparseArray[entity.ID] = entity; + + // FIXME: something is not right here + if (entity.ID >= ElementArray.Count) + { + ElementArray.ResizeTo(entity.ID + 1); + } + } + + ElementArray.Set(entity.ID, component); + return !newEntity; + } + + public bool Has(Entity entity) + { + return SparseArray[entity.ID] != Entity.Null; + } + + public bool Remove(Entity entity) + { + if (Has(entity)) + { + var denseIndex = SparseArray[entity.ID]; + var lastItem = DenseArray[DenseArray.Count - 1]; + DenseArray[denseIndex.ID] = lastItem; + SparseArray[lastItem.ID] = denseIndex; + SparseArray[entity.ID] = Entity.Null; // sentinel value + DenseArray.RemoveLastElement(); + ElementArray.RemoveLastElement(); return true; } @@ -93,26 +101,29 @@ internal class ComponentStorage : IDisposable public void Clear() { - Components.Clear(); - EntityIDs.Clear(); - EntityIDToStorageIndex.Clear(); + DenseArray.Clear(); + ElementArray.Clear(); + for (var i = 0; i < SparseArray.Count; i += 1) + { + SparseArray[i] = Entity.Null; + } } public Entity FirstEntity() { #if DEBUG - if (EntityIDs.Count == 0) + if (DenseArray.Count == 0) { throw new IndexOutOfRangeException("Component storage is empty!"); } #endif - return EntityIDs[0]; + return DenseArray[0]; } #if DEBUG - internal IEnumerable Debug_GetEntities() + internal Span Debug_GetEntities() { - return EntityIDToStorageIndex.Keys; + return DenseArray.ToSpan(); } #endif @@ -120,19 +131,17 @@ internal class ComponentStorage : IDisposable { if (!IsDisposed) { - Components.Dispose(); - EntityIDs.Dispose(); + if (disposing) + { + DenseArray.Dispose(); + SparseArray.Dispose(); + ElementArray.Dispose(); + } IsDisposed = true; } } - // ~ComponentStorage() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method diff --git a/src/DebugSystem.cs b/src/DebugSystem.cs index 240614a..3cb1173 100644 --- a/src/DebugSystem.cs +++ b/src/DebugSystem.cs @@ -11,7 +11,7 @@ public abstract class DebugSystem : System protected DebugSystem(World world) : base(world) { } protected World.ComponentTypeEnumerator Debug_GetAllComponentTypes(Entity entity) => World.Debug_GetAllComponentTypes(entity); - protected IEnumerable Debug_GetEntities(Type componentType) => World.Debug_GetEntities(componentType); + protected Span Debug_GetEntities(Type componentType) => World.Debug_GetEntities(componentType); protected IEnumerable Debug_SearchComponentType(string typeString) => World.Debug_SearchComponentType(typeString); } #endif diff --git a/src/Entity.cs b/src/Entity.cs index 02ad024..c2e866a 100644 --- a/src/Entity.cs +++ b/src/Entity.cs @@ -1,3 +1,6 @@ namespace MoonTools.ECS; -public readonly record struct Entity(uint ID); +public readonly record struct Entity(int ID) +{ + public static readonly Entity Null = new Entity(int.MaxValue); +} diff --git a/src/IdAssigner.cs b/src/IdAssigner.cs index a31e73b..6a329fd 100644 --- a/src/IdAssigner.cs +++ b/src/IdAssigner.cs @@ -5,12 +5,12 @@ namespace MoonTools.ECS; internal class IdAssigner : IDisposable { - uint Next; - NativeArray AvailableIds = new NativeArray(); + int Next = 0; + NativeArray AvailableIds = new NativeArray(); private bool IsDisposed; - public uint Assign() + public int Assign() { if (AvailableIds.TryPop(out var id)) { @@ -22,7 +22,7 @@ internal class IdAssigner : IDisposable return id; } - public void Unassign(uint id) + public void Unassign(int id) { AvailableIds.Append(id); } @@ -46,13 +46,6 @@ internal class IdAssigner : IDisposable } } - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~IdAssigner() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method diff --git a/src/Snapshot.cs b/src/Snapshot.cs index 9090852..def9fc9 100644 --- a/src/Snapshot.cs +++ b/src/Snapshot.cs @@ -206,34 +206,31 @@ public class Snapshot : IDisposable private class ComponentSnapshot : IDisposable { - private readonly NativeArray Components; - private readonly NativeArray EntityIDs; + private readonly NativeArray DenseArray = new NativeArray(); + private readonly NativeArray SparseArray = new NativeArray(); + private readonly NativeArray ElementArray; private bool IsDisposed; public ComponentSnapshot(int elementSize) { - Components = new NativeArray(elementSize); - EntityIDs = new NativeArray(); + ElementArray = new NativeArray(elementSize); + DenseArray = new NativeArray(elementSize); + SparseArray = new NativeArray(elementSize); } public void Take(ComponentStorage componentStorage) { - componentStorage.Components.CopyAllTo(Components); - componentStorage.EntityIDs.CopyTo(EntityIDs); + componentStorage.DenseArray.CopyTo(DenseArray); + componentStorage.SparseArray.CopyTo(SparseArray); + componentStorage.ElementArray.CopyAllTo(ElementArray); } public void Restore(ComponentStorage componentStorage) { - Components.CopyAllTo(componentStorage.Components); - EntityIDs.CopyTo(componentStorage.EntityIDs); - - componentStorage.EntityIDToStorageIndex.Clear(); - for (int i = 0; i < EntityIDs.Count; i += 1) - { - var entityID = EntityIDs[i]; - componentStorage.EntityIDToStorageIndex[entityID] = i; - } + DenseArray.CopyTo(componentStorage.DenseArray); + SparseArray.CopyTo(componentStorage.SparseArray); + ElementArray.CopyAllTo(componentStorage.ElementArray); } protected virtual void Dispose(bool disposing) @@ -242,8 +239,9 @@ public class Snapshot : IDisposable { if (disposing) { - Components.Dispose(); - EntityIDs.Dispose(); + DenseArray.Dispose(); + SparseArray.Dispose(); + ElementArray.Dispose(); } IsDisposed = true; diff --git a/src/World.cs b/src/World.cs index 9ef9ff7..0f975d7 100644 --- a/src/World.cs +++ b/src/World.cs @@ -51,8 +51,7 @@ public class World : IDisposable // add missing storages, it's possible for there to be multiples in multi-world scenarios for (var i = ComponentIndex.Count; i <= typeId; i += 1) { - var missingTypeId = new TypeId((uint) i); - var componentStorage = new ComponentStorage(missingTypeId, ComponentTypeElementSizes[i]); + var componentStorage = new ComponentStorage(ComponentTypeElementSizes[i]); ComponentIndex.Add(componentStorage); ComponentTypeToFilter.Add(new List()); } @@ -149,8 +148,7 @@ public class World : IDisposable public bool Has(in Entity entity) where T : unmanaged { - var storage = GetComponentStorage(); - return storage.Has(entity); + return EntityComponentIndex[entity].Contains(new TypeId(ComponentTypeIdAssigner.Id)); } internal bool Has(in Entity entity, in TypeId typeId) @@ -185,12 +183,13 @@ public class World : IDisposable public void Set(in Entity entity, in T component) where T : unmanaged { var componentStorage = GetComponentStorage(); + var typeId = new TypeId(ComponentTypeIdAssigner.Id); if (!componentStorage.Set(entity, component)) { - EntityComponentIndex[entity].Add(componentStorage.TypeId); + EntityComponentIndex[entity].Add(typeId); - foreach (var filter in ComponentTypeToFilter[componentStorage.TypeId]) + foreach (var filter in ComponentTypeToFilter[typeId]) { filter.Check(entity); } @@ -200,12 +199,13 @@ public class World : IDisposable public void Remove(in Entity entity) where T : unmanaged { var componentStorage = GetComponentStorage(); + var typeId = new TypeId(ComponentTypeIdAssigner.Id); if (componentStorage.Remove(entity)) { - EntityComponentIndex[entity].Remove(componentStorage.TypeId); + EntityComponentIndex[entity].Remove(typeId); - foreach (var filter in ComponentTypeToFilter[componentStorage.TypeId]) + foreach (var filter in ComponentTypeToFilter[typeId]) { filter.Check(entity); } @@ -391,7 +391,7 @@ public class World : IDisposable return new ComponentTypeEnumerator(this, EntityComponentIndex[entity]); } - public IEnumerable Debug_GetEntities(Type componentType) + public Span Debug_GetEntities(Type componentType) { var storage = ComponentIndex[ComponentTypeToId[componentType]]; return storage.Debug_GetEntities();