From 40d2bf87bac65eb0e3be257ed3bb9147c545001e Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Thu, 19 Oct 2023 15:41:49 -0700 Subject: [PATCH] initial archetype storage structure --- src/Rev2/Archetype.cs | 22 +++++ src/Rev2/ArchetypeEdge.cs | 4 + src/Rev2/ArchetypeId.cs | 4 + src/Rev2/ArchetypeRecord.cs | 4 + src/Rev2/ArchetypeSignature.cs | 82 ++++++++++++++++++ src/Rev2/Column.cs | 63 ++++++++++++++ src/Rev2/ComponentId.cs | 4 + src/Rev2/EntityId.cs | 4 + src/Rev2/HasId.cs | 6 ++ src/Rev2/IdAssigner.cs | 26 ++++++ src/Rev2/Record.cs | 4 + src/Rev2/World.cs | 148 +++++++++++++++++++++++++++++++++ 12 files changed, 371 insertions(+) create mode 100644 src/Rev2/Archetype.cs create mode 100644 src/Rev2/ArchetypeEdge.cs create mode 100644 src/Rev2/ArchetypeId.cs create mode 100644 src/Rev2/ArchetypeRecord.cs create mode 100644 src/Rev2/ArchetypeSignature.cs create mode 100644 src/Rev2/Column.cs create mode 100644 src/Rev2/ComponentId.cs create mode 100644 src/Rev2/EntityId.cs create mode 100644 src/Rev2/HasId.cs create mode 100644 src/Rev2/IdAssigner.cs create mode 100644 src/Rev2/Record.cs create mode 100644 src/Rev2/World.cs diff --git a/src/Rev2/Archetype.cs b/src/Rev2/Archetype.cs new file mode 100644 index 0000000..4e69e7e --- /dev/null +++ b/src/Rev2/Archetype.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace MoonTools.ECS.Rev2 +{ + public class Archetype + { + public ArchetypeSignature Signature; + public ArchetypeId Id { get; private set; } + /* FIXME: make this native memory too */ + public List Components = new List(); + public Dictionary Edges = new Dictionary(); + + public int Count; + + public Archetype(ArchetypeId id, ArchetypeSignature signature) + { + Id = id; + Signature = signature; + Count = 0; + } + } +} diff --git a/src/Rev2/ArchetypeEdge.cs b/src/Rev2/ArchetypeEdge.cs new file mode 100644 index 0000000..f02782c --- /dev/null +++ b/src/Rev2/ArchetypeEdge.cs @@ -0,0 +1,4 @@ +namespace MoonTools.ECS.Rev2 +{ + public readonly record struct ArchetypeEdge(Archetype Add, Archetype Remove); +} diff --git a/src/Rev2/ArchetypeId.cs b/src/Rev2/ArchetypeId.cs new file mode 100644 index 0000000..52b7f0d --- /dev/null +++ b/src/Rev2/ArchetypeId.cs @@ -0,0 +1,4 @@ +namespace MoonTools.ECS.Rev2 +{ + public readonly record struct ArchetypeId(int Id) : IHasId; +} diff --git a/src/Rev2/ArchetypeRecord.cs b/src/Rev2/ArchetypeRecord.cs new file mode 100644 index 0000000..784699f --- /dev/null +++ b/src/Rev2/ArchetypeRecord.cs @@ -0,0 +1,4 @@ +namespace MoonTools.ECS.Rev2 +{ + public readonly record struct ArchetypeRecord(int ColumnIndex); +} diff --git a/src/Rev2/ArchetypeSignature.cs b/src/Rev2/ArchetypeSignature.cs new file mode 100644 index 0000000..747fe9d --- /dev/null +++ b/src/Rev2/ArchetypeSignature.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace MoonTools.ECS.Rev2 +{ + public class ArchetypeSignature : IEquatable + { + public static ArchetypeSignature Empty = new ArchetypeSignature(0); + + List Ids; + + public int Count => Ids.Count; + + public ComponentId this[int i] => new ComponentId(Ids[i]); + + public ArchetypeSignature() + { + Ids = new List(); + } + + public ArchetypeSignature(int capacity) + { + Ids = new List(capacity); + } + + // Maintains sorted order + public void Insert(ComponentId componentId) + { + var index = Ids.BinarySearch(componentId.Id); + + if (index < 0) + { + Ids.Insert(~index, componentId.Id); + } + } + + public void CopyTo(ArchetypeSignature other) + { + other.Ids.AddRange(Ids); + } + + public override bool Equals(object? obj) + { + return obj is ArchetypeSignature signature && Equals(signature); + } + + public bool Equals(ArchetypeSignature? other) + { + if (other == null) + { + return false; + } + + if (Ids.Count != other.Ids.Count) + { + return false; + } + + for (int i = 0; i < Ids.Count; i += 1) + { + if (Ids[i] != other.Ids[i]) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + var hashcode = 1; + + foreach (var id in Ids) + { + hashcode = HashCode.Combine(hashcode, id); + } + + return hashcode; + } + } +} diff --git a/src/Rev2/Column.cs b/src/Rev2/Column.cs new file mode 100644 index 0000000..6675313 --- /dev/null +++ b/src/Rev2/Column.cs @@ -0,0 +1,63 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace MoonTools.ECS.Rev2 +{ + public unsafe class Column + { + public nint Elements; + public int ElementSize; + public int Count; + public int Capacity; + + public static Column Create() where T : unmanaged + { + return new Column(Unsafe.SizeOf()); + } + + private Column(int elementSize) + { + Capacity = 16; + Count = 0; + ElementSize = elementSize; + + Elements = (nint) NativeMemory.Alloc((nuint) (ElementSize * Capacity)); + } + + private void Resize() + { + Capacity *= 2; + Elements = (nint) NativeMemory.Realloc((void*) Elements, (nuint) (ElementSize * Capacity)); + } + + public void Delete(int index) + { + if (Count > 1) + { + NativeMemory.Copy( + (void*) (Elements + ((Count - 1) * ElementSize)), + (void*) (Elements + (index * ElementSize)), + (nuint) ElementSize + ); + } + + Count -= 1; + } + + public void CopyToEnd(int index, Column other) + { + if (other.Count >= other.Capacity) + { + other.Resize(); + } + + NativeMemory.Copy( + (void*) (Elements + (index * ElementSize)), + (void*) (other.Elements + (other.Count * ElementSize)), + (nuint) ElementSize + ); + + other.Count += 1; + } + } +} diff --git a/src/Rev2/ComponentId.cs b/src/Rev2/ComponentId.cs new file mode 100644 index 0000000..b2b24b8 --- /dev/null +++ b/src/Rev2/ComponentId.cs @@ -0,0 +1,4 @@ +namespace MoonTools.ECS.Rev2 +{ + public readonly record struct ComponentId(int Id) : IHasId; +} diff --git a/src/Rev2/EntityId.cs b/src/Rev2/EntityId.cs new file mode 100644 index 0000000..ee0c5d7 --- /dev/null +++ b/src/Rev2/EntityId.cs @@ -0,0 +1,4 @@ +namespace MoonTools.ECS.Rev2 +{ + public readonly record struct EntityId(int Id) : IHasId; +} diff --git a/src/Rev2/HasId.cs b/src/Rev2/HasId.cs new file mode 100644 index 0000000..69e824a --- /dev/null +++ b/src/Rev2/HasId.cs @@ -0,0 +1,6 @@ +namespace MoonTools.ECS.Rev2; + +public interface IHasId +{ + public int Id { get; init; } +} diff --git a/src/Rev2/IdAssigner.cs b/src/Rev2/IdAssigner.cs new file mode 100644 index 0000000..0da7de6 --- /dev/null +++ b/src/Rev2/IdAssigner.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace MoonTools.ECS.Rev2 +{ + public class IdAssigner where T : struct, IHasId + { + int Next; + Queue AvailableIds = new Queue(); + + public T Assign() + { + if (!AvailableIds.TryDequeue(out var id)) + { + id = Next; + Next += 1; + } + + return new T { Id = id }; + } + + public void Unassign(T idHaver) + { + AvailableIds.Enqueue(idHaver.Id); + } + } +} diff --git a/src/Rev2/Record.cs b/src/Rev2/Record.cs new file mode 100644 index 0000000..653ff4c --- /dev/null +++ b/src/Rev2/Record.cs @@ -0,0 +1,4 @@ +namespace MoonTools.ECS.Rev2 +{ + public readonly record struct Record(Archetype Archetype, int Row); +} diff --git a/src/Rev2/World.cs b/src/Rev2/World.cs new file mode 100644 index 0000000..c9f98cf --- /dev/null +++ b/src/Rev2/World.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; + +namespace MoonTools.ECS.Rev2 +{ + public class World + { + // Lookup from ArchetypeSignature to Archetype + Dictionary ArchetypeIndex = new Dictionary(); + + // Going from EntityId to Archetype and storage row + Dictionary EntityIndex = new Dictionary(); + + // Going from ComponentId to an Archetype storage column + Dictionary> ComponentIndex = new Dictionary>(); + + // Get ComponentId from a Type + Dictionary TypeToComponentId = new Dictionary(); + + // ID Management + IdAssigner ArchetypeIdAssigner = new IdAssigner(); + IdAssigner EntityIdAssigner = new IdAssigner(); + IdAssigner ComponentIdAssigner = new IdAssigner(); + + public World() + { + // Create the Empty Archetype + CreateArchetype(ArchetypeSignature.Empty); + } + + private Archetype CreateArchetype(ArchetypeSignature signature) + { + var archetypeId = ArchetypeIdAssigner.Assign(); + var archetype = new Archetype(archetypeId, signature); + ArchetypeIndex.Add(signature, archetype); + + for (int i = 0; i < signature.Count; i += 1) + { + var componentId = signature[i]; + ComponentIndex[componentId].Add(archetypeId, new ArchetypeRecord(i)); + } + + return archetype; + } + + public EntityId CreateEntity() + { + var entityId = EntityIdAssigner.Assign(); + var emptyArchetype = ArchetypeIndex[ArchetypeSignature.Empty]; + EntityIndex.Add(entityId, new Record(emptyArchetype, 0)); + return entityId; + } + + // FIXME: would be much more efficient to do all this at load time somehow + private void RegisterComponentId(ComponentId componentId) + { + ComponentIndex.Add(componentId, new Dictionary()); + } + + public ComponentId GetComponentId() where T : unmanaged + { + if (!TypeToComponentId.TryGetValue(typeof(T), out var componentId)) + { + componentId = ComponentIdAssigner.Assign(); + } + + return componentId; + } + + public bool HasComponent(EntityId entityId) where T : unmanaged + { + var componentId = GetComponentId(); + + var record = EntityIndex[entityId]; + var archetypes = ComponentIndex[componentId]; + return archetypes.ContainsKey(record.Archetype.Id); + } + + public unsafe T GetComponent(EntityId entityId) where T : unmanaged + { + var componentId = GetComponentId(); + + var record = EntityIndex[entityId]; + var archetype = record.Archetype; + + var archetypes = ComponentIndex[componentId]; + if (!archetypes.ContainsKey(archetype.Id)) + { + return default; // FIXME: maybe throw in debug mode? + } + + var archetypeRecord = archetypes[archetype.Id]; + var column = archetype.Components[archetypeRecord.ColumnIndex]; + + return ((T*) column.Elements)[record.Row]; + } + + public void AddComponent(EntityId entityId, T component) where T : unmanaged + { + Archetype? nextArchetype; + + var componentId = GetComponentId(); + + var record = EntityIndex[entityId]; + var archetype = record.Archetype; + + if (archetype.Edges.TryGetValue(componentId, out var edge)) + { + nextArchetype = edge.Add; + } + else + { + // FIXME: pool the signatures + var nextSignature = new ArchetypeSignature(archetype.Signature.Count + 1); + archetype.Signature.CopyTo(nextSignature); + nextSignature.Insert(componentId); + + if (!ArchetypeIndex.TryGetValue(nextSignature, out nextArchetype)) + { + nextArchetype = CreateArchetype(nextSignature); + } + + var newEdge = new ArchetypeEdge(archetype, nextArchetype); + archetype.Edges.Add(componentId, newEdge); + nextArchetype.Edges.Add(componentId, newEdge); + } + + MoveEntity(entityId, record.Row, archetype, nextArchetype); + } + + private void MoveEntity(EntityId entityId, int row, Archetype from, Archetype to) + { + for (int i = 0; i < from.Components.Count; i += 1) + { + var componentId = from.Signature[i]; + var destinationColumnIndex = ComponentIndex[componentId][to.Id].ColumnIndex; + + from.Components[i].CopyToEnd(row, to.Components[destinationColumnIndex]); + from.Components[i].Delete(row); + } + + EntityIndex[entityId] = new Record(to, to.Count); + + to.Count += 1; + from.Count -= 1; + } + } +}