From 5419fbd72d173485907923742fe5357b094e4f22 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 24 Oct 2023 18:44:41 -0700 Subject: [PATCH] snapshot system --- src/Rev2/Archetype.cs | 44 ++++++++----- src/Rev2/ArchetypeEdge.cs | 2 +- src/Rev2/ArchetypeId.cs | 4 -- src/Rev2/ArchetypeSignature.cs | 2 +- src/Rev2/Column.cs | 31 ++++++++-- src/Rev2/ComponentId.cs | 2 +- src/Rev2/Filter.cs | 54 ++++++++-------- src/Rev2/FilterBuilder.cs | 42 +++++++++++++ src/Rev2/IdAssigner.cs | 2 +- src/Rev2/Record.cs | 2 +- src/Rev2/Snapshot.cs | 110 +++++++++++++++++++++++++++++++++ src/Rev2/World.cs | 95 +++++++++++++++++----------- 12 files changed, 300 insertions(+), 90 deletions(-) delete mode 100644 src/Rev2/ArchetypeId.cs create mode 100644 src/Rev2/FilterBuilder.cs create mode 100644 src/Rev2/Snapshot.cs diff --git a/src/Rev2/Archetype.cs b/src/Rev2/Archetype.cs index 36aeafb..c0442f0 100644 --- a/src/Rev2/Archetype.cs +++ b/src/Rev2/Archetype.cs @@ -1,24 +1,38 @@ using System.Collections.Generic; -namespace MoonTools.ECS.Rev2 +namespace MoonTools.ECS.Rev2; + +internal class Archetype { - public class Archetype + public World World; + public ArchetypeSignature Signature; + public List ComponentColumns = new List(); + public List RowToEntity = new List(); + + public Dictionary ComponentToColumnIndex = + new Dictionary(); + public SortedDictionary Edges = new SortedDictionary(); + + public int Count => RowToEntity.Count; + + public Archetype(World world, ArchetypeSignature signature) { - public ArchetypeSignature Signature; - public ArchetypeId Id { get; private set; } - public List Components = new List(); - public List RowToEntity = new List(); + World = world; + Signature = signature; + } - public Dictionary ComponentToColumnIndex = - new Dictionary(); - public SortedDictionary Edges = new SortedDictionary(); - - public int Count => RowToEntity.Count; - - public Archetype(ArchetypeId id, ArchetypeSignature signature) + public void ClearAll() + { + for (int i = 0; i < ComponentColumns.Count; i += 1) { - Id = id; - Signature = signature; + ComponentColumns[i].Count = 0; } + + foreach (var entityId in RowToEntity) + { + World.FreeEntity(entityId); + } + + RowToEntity.Clear(); } } diff --git a/src/Rev2/ArchetypeEdge.cs b/src/Rev2/ArchetypeEdge.cs index f02782c..3898911 100644 --- a/src/Rev2/ArchetypeEdge.cs +++ b/src/Rev2/ArchetypeEdge.cs @@ -1,4 +1,4 @@ namespace MoonTools.ECS.Rev2 { - public readonly record struct ArchetypeEdge(Archetype Add, Archetype Remove); + internal readonly record struct ArchetypeEdge(Archetype Add, Archetype Remove); } diff --git a/src/Rev2/ArchetypeId.cs b/src/Rev2/ArchetypeId.cs deleted file mode 100644 index 52b7f0d..0000000 --- a/src/Rev2/ArchetypeId.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace MoonTools.ECS.Rev2 -{ - public readonly record struct ArchetypeId(int Id) : IHasId; -} diff --git a/src/Rev2/ArchetypeSignature.cs b/src/Rev2/ArchetypeSignature.cs index b904dd3..728d5e2 100644 --- a/src/Rev2/ArchetypeSignature.cs +++ b/src/Rev2/ArchetypeSignature.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace MoonTools.ECS.Rev2 { - public class ArchetypeSignature : IEquatable + internal class ArchetypeSignature : IEquatable { public static ArchetypeSignature Empty = new ArchetypeSignature(0); diff --git a/src/Rev2/Column.cs b/src/Rev2/Column.cs index 1a7d984..632a66c 100644 --- a/src/Rev2/Column.cs +++ b/src/Rev2/Column.cs @@ -3,12 +3,13 @@ using System.Runtime.InteropServices; namespace MoonTools.ECS.Rev2 { - public unsafe class Column : IDisposable + internal unsafe class Column : IDisposable { public nint Elements; - public int ElementSize; public int Count; - public int Capacity; + + private int Capacity; + private readonly int ElementSize; private bool IsDisposed; @@ -27,6 +28,12 @@ namespace MoonTools.ECS.Rev2 Elements = (nint) NativeMemory.Realloc((void*) Elements, (nuint) (ElementSize * Capacity)); } + private void ResizeTo(int capacity) + { + Capacity = capacity; + Elements = (nint) NativeMemory.Realloc((void*) Elements, (nuint) (ElementSize * Capacity)); + } + // Fills gap by copying final element to the deleted index public void Delete(int index) { @@ -53,7 +60,7 @@ namespace MoonTools.ECS.Rev2 Count += 1; } - public void CopyToEnd(int index, Column other) + public void CopyElementToEnd(int index, Column other) { if (other.Count >= other.Capacity) { @@ -69,6 +76,22 @@ namespace MoonTools.ECS.Rev2 other.Count += 1; } + public void CopyAllTo(Column other) + { + if (Count >= other.Capacity) + { + other.ResizeTo(Count); + } + + NativeMemory.Copy( + (void*) Elements, + (void*) other.Elements, + (nuint) (ElementSize * Count) + ); + + other.Count = Count; + } + protected virtual void Dispose(bool disposing) { if (!IsDisposed) diff --git a/src/Rev2/ComponentId.cs b/src/Rev2/ComponentId.cs index 5e11e0e..8d59b3e 100644 --- a/src/Rev2/ComponentId.cs +++ b/src/Rev2/ComponentId.cs @@ -2,7 +2,7 @@ using System; namespace MoonTools.ECS.Rev2 { - public readonly record struct ComponentId(int Id) : IHasId, IComparable + internal readonly record struct ComponentId(int Id) : IHasId, IComparable { public int CompareTo(ComponentId other) { diff --git a/src/Rev2/Filter.cs b/src/Rev2/Filter.cs index b438076..aea4010 100644 --- a/src/Rev2/Filter.cs +++ b/src/Rev2/Filter.cs @@ -47,6 +47,15 @@ namespace MoonTools.ECS.Rev2 } } + public EntityId RandomEntity + { + get + { + var randomIndex = RandomManager.Next(Count); + return NthEntity(randomIndex); + } + } + // WARNING: this WILL crash if the index is out of range! public EntityId NthEntity(int index) { @@ -66,16 +75,25 @@ namespace MoonTools.ECS.Rev2 throw new InvalidOperationException("Filter index out of range!"); } - public EntityId RandomEntity + public void DestroyAllEntities() { - get + foreach (var archetype in Archetypes) { - var randomIndex = RandomManager.Next(Count); - return NthEntity(randomIndex); + archetype.ClearAll(); } } - public Filter(Archetype emptyArchetype, HashSet included, HashSet excluded) + public void TakeSnapshot(Snapshot snapshot) + { + snapshot.Reset(); + + foreach (var archetype in Archetypes) + { + snapshot.TakeArchetypeSnapshot(archetype); + } + } + + internal Filter(Archetype emptyArchetype, HashSet included, HashSet excluded) { EmptyArchetype = emptyArchetype; Included = included; @@ -85,7 +103,6 @@ namespace MoonTools.ECS.Rev2 internal ref struct ArchetypeEnumerator { private Archetype CurrentArchetype; - private bool Active; // TODO: pool these private Queue ArchetypeQueue = new Queue(); @@ -96,8 +113,6 @@ namespace MoonTools.ECS.Rev2 public ArchetypeEnumerator(Filter filter) { - Active = false; - var empty = filter.EmptyArchetype; ArchetypeSearchQueue.Enqueue(empty); @@ -127,27 +142,14 @@ namespace MoonTools.ECS.Rev2 ArchetypeQueue.Enqueue(current); } - // if the current archetype satisfies the filter, we need to add all edges that - // do not have an excluded component - // if the current archetype does not satisfy the filter, we need to add all edges that - // include an included component + // breadth-first search + // ignore excluded component edges foreach (var (componentId, edge) in current.Edges) { - if (satisfiesFilter) + if (!Explored.Contains(edge.Add) && !filter.Excluded.Contains(componentId)) { - if (!filter.Excluded.Contains(componentId) && !Explored.Contains(edge.Add)) - { - Explored.Add(edge.Add); - ArchetypeSearchQueue.Enqueue(edge.Add); - } - } - else - { - if (filter.Included.Contains(componentId) && !Explored.Contains(edge.Add)) - { - Explored.Add(edge.Add); - ArchetypeSearchQueue.Enqueue(edge.Add); - } + Explored.Add(edge.Add); + ArchetypeSearchQueue.Enqueue(edge.Add); } } } diff --git a/src/Rev2/FilterBuilder.cs b/src/Rev2/FilterBuilder.cs new file mode 100644 index 0000000..5edc8b8 --- /dev/null +++ b/src/Rev2/FilterBuilder.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace MoonTools.ECS.Rev2 +{ + public ref struct FilterBuilder + { + World World; + HashSet Included; + HashSet Excluded; + + internal FilterBuilder(World world) + { + World = world; + Included = new HashSet(); + Excluded = new HashSet(); + } + + private FilterBuilder(World world, HashSet included, HashSet excluded) + { + World = world; + Included = included; + Excluded = excluded; + } + + public FilterBuilder Include() where T : unmanaged + { + Included.Add(World.TypeToComponentId[typeof(T)]); + return new FilterBuilder(World, Included, Excluded); + } + + public FilterBuilder Exclude() where T : unmanaged + { + Excluded.Add(World.TypeToComponentId[typeof(T)]); + return new FilterBuilder(World, Included, Excluded); + } + + public Filter Build() + { + return new Filter(World.EmptyArchetype, Included, Excluded); + } + } +} diff --git a/src/Rev2/IdAssigner.cs b/src/Rev2/IdAssigner.cs index 0da7de6..c797b43 100644 --- a/src/Rev2/IdAssigner.cs +++ b/src/Rev2/IdAssigner.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace MoonTools.ECS.Rev2 { - public class IdAssigner where T : struct, IHasId + internal class IdAssigner where T : struct, IHasId { int Next; Queue AvailableIds = new Queue(); diff --git a/src/Rev2/Record.cs b/src/Rev2/Record.cs index 653ff4c..f997344 100644 --- a/src/Rev2/Record.cs +++ b/src/Rev2/Record.cs @@ -1,4 +1,4 @@ namespace MoonTools.ECS.Rev2 { - public readonly record struct Record(Archetype Archetype, int Row); + internal readonly record struct Record(Archetype Archetype, int Row); } diff --git a/src/Rev2/Snapshot.cs b/src/Rev2/Snapshot.cs new file mode 100644 index 0000000..d510e09 --- /dev/null +++ b/src/Rev2/Snapshot.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; + +namespace MoonTools.ECS.Rev2; + +public class Snapshot +{ + private Dictionary ArchetypeSnapshots = + new Dictionary(); + + public int Count + { + get + { + var count = 0; + + foreach (var snapshot in ArchetypeSnapshots.Values) + { + count += snapshot.Count; + } + + return count; + } + } + + public void Restore(World world) + { + foreach (var (archetypeSignature, archetypeSnapshot) in ArchetypeSnapshots) + { + var archetype = world.ArchetypeIndex[archetypeSignature]; + RestoreArchetypeSnapshot(archetype); + } + } + + internal void Reset() + { + foreach (var archetypeSnapshot in ArchetypeSnapshots.Values) + { + archetypeSnapshot.Count = 0; + } + } + + internal void TakeArchetypeSnapshot(Archetype archetype) + { + if (!ArchetypeSnapshots.TryGetValue(archetype.Signature, out var archetypeSnapshot)) + { + archetypeSnapshot = new ArchetypeSnapshot(archetype.Signature); + ArchetypeSnapshots.Add(archetype.Signature, archetypeSnapshot); + } + + archetypeSnapshot.Take(archetype); + } + + private void RestoreArchetypeSnapshot(Archetype archetype) + { + var archetypeSnapshot = ArchetypeSnapshots[archetype.Signature]; + archetypeSnapshot.Restore(archetype); + } + + private class ArchetypeSnapshot + { + public ArchetypeSignature Signature; + public readonly List ComponentColumns; + + public int Count; + + public ArchetypeSnapshot(ArchetypeSignature signature) + { + Signature = signature; + ComponentColumns = new List(signature.Count); + + for (int i = 0; i < signature.Count; i += 1) + { + var componentId = signature[i]; + ComponentColumns.Add(new Column(World.ElementSizes[componentId])); + } + } + + public void Take(Archetype archetype) + { + for (int i = 0; i < ComponentColumns.Count; i += 1) + { + archetype.ComponentColumns[i].CopyAllTo(ComponentColumns[i]); + } + + Count = archetype.Count; + } + + public void Restore(Archetype archetype) + { + // Clear out existing entities + archetype.ClearAll(); + + // Copy all component data + for (int i = 0; i < ComponentColumns.Count; i += 1) + { + ComponentColumns[i].CopyAllTo(archetype.ComponentColumns[i]); + } + + // Clear the row to entity list + archetype.RowToEntity.Clear(); + + // Create new entities and repopulate the row to entity list + for (int i = 0; i < Count; i += 1) + { + var entityId = archetype.World.CreateEntityOnArchetype(archetype); + archetype.RowToEntity.Add(entityId); + } + } + } +} diff --git a/src/Rev2/World.cs b/src/Rev2/World.cs index cbdedfe..c981560 100644 --- a/src/Rev2/World.cs +++ b/src/Rev2/World.cs @@ -7,8 +7,13 @@ namespace MoonTools.ECS.Rev2 { public class World : IDisposable { + // Get ComponentId from a Type + internal static Dictionary TypeToComponentId = new Dictionary(); + // Get element size from a ComponentId + internal static Dictionary ElementSizes = new Dictionary(); + // Lookup from ArchetypeSignature to Archetype - Dictionary ArchetypeIndex = new Dictionary(); + internal Dictionary ArchetypeIndex = new Dictionary(); // Going from EntityId to Archetype and storage row Dictionary EntityIndex = new Dictionary(); @@ -16,18 +21,13 @@ namespace MoonTools.ECS.Rev2 // Going from ComponentId to Archetype list Dictionary> ComponentIndex = new Dictionary>(); - // Get ComponentId from a Type - Dictionary TypeToComponentId = new Dictionary(); - - // Get element size from a ComponentId - Dictionary ElementSizes = new Dictionary(); - // ID Management - IdAssigner ArchetypeIdAssigner = new IdAssigner(); IdAssigner EntityIdAssigner = new IdAssigner(); IdAssigner ComponentIdAssigner = new IdAssigner(); - public readonly Archetype EmptyArchetype; + internal readonly Archetype EmptyArchetype; + + public FilterBuilder FilterBuilder => new FilterBuilder(this); private bool IsDisposed; @@ -39,12 +39,11 @@ namespace MoonTools.ECS.Rev2 EmptyArchetype = CreateArchetype(ArchetypeSignature.Empty); } - private Archetype CreateArchetype(ArchetypeSignature signature) + internal Archetype CreateArchetype(ArchetypeSignature signature) { - var archetypeId = ArchetypeIdAssigner.Assign(); - var archetype = new Archetype(archetypeId, signature) + var archetype = new Archetype(this, signature) { - Components = new List(signature.Count) + ComponentColumns = new List(signature.Count) }; ArchetypeIndex.Add(signature, archetype); @@ -52,9 +51,9 @@ namespace MoonTools.ECS.Rev2 for (int i = 0; i < signature.Count; i += 1) { var componentId = signature[i]; - ComponentIndex[componentId].Add(archetype); //, new ArchetypeRecord(i)); - archetype.ComponentToColumnIndex.Add(componentId, archetype.Components.Count); - archetype.Components.Add(new Column(ElementSizes[componentId])); + ComponentIndex[componentId].Add(archetype); + archetype.ComponentToColumnIndex.Add(componentId, archetype.ComponentColumns.Count); + archetype.ComponentColumns.Add(new Column(ElementSizes[componentId])); } return archetype; @@ -63,12 +62,27 @@ namespace MoonTools.ECS.Rev2 public EntityId CreateEntity() { var entityId = EntityIdAssigner.Assign(); - var emptyArchetype = ArchetypeIndex[ArchetypeSignature.Empty]; - EntityIndex.Add(entityId, new Record(emptyArchetype, emptyArchetype.Count)); - emptyArchetype.RowToEntity.Add(entityId); + EntityIndex.Add(entityId, new Record(EmptyArchetype, EmptyArchetype.Count)); + EmptyArchetype.RowToEntity.Add(entityId); return entityId; } + // used as a fast path by Archetype.Transfer + internal EntityId CreateEntityOnArchetype(Archetype archetype) + { + var entityId = EntityIdAssigner.Assign(); + EntityIndex.Add(entityId, new Record(archetype, archetype.Count)); + archetype.RowToEntity.Add(entityId); + return entityId; + } + + // used as a fast path by Archetype.ClearAll + internal void FreeEntity(EntityId entityId) + { + EntityIndex.Remove(entityId); + EntityIdAssigner.Unassign(entityId); + } + // FIXME: would be much more efficient to do all this at load time somehow private void RegisterComponent() where T : unmanaged { @@ -80,12 +94,21 @@ namespace MoonTools.ECS.Rev2 private void TryRegisterComponentId() where T : unmanaged { - if (!TypeToComponentId.TryGetValue(typeof(T), out var componentId)) + if (!TypeToComponentId.ContainsKey(typeof(T))) { RegisterComponent(); } } + // non-generic variant for use with Transfer + internal void AddComponentIndexEntry(ComponentId componentId) + { + if (!ComponentIndex.ContainsKey(componentId)) + { + ComponentIndex.Add(componentId, new List()); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private ComponentId GetComponentId() where T : unmanaged { @@ -106,7 +129,7 @@ namespace MoonTools.ECS.Rev2 var record = EntityIndex[entityId]; var columnIndex = record.Archetype.ComponentToColumnIndex[componentId]; - var column = record.Archetype.Components[columnIndex]; + var column = record.Archetype.ComponentColumns[columnIndex]; return ref ((T*) column.Elements)[record.Row]; } @@ -120,7 +143,7 @@ namespace MoonTools.ECS.Rev2 { var record = EntityIndex[entityId]; var columnIndex = record.Archetype.ComponentToColumnIndex[componentId]; - var column = record.Archetype.Components[columnIndex]; + var column = record.Archetype.ComponentColumns[columnIndex]; ((T*) column.Elements)[record.Row] = component; } @@ -165,7 +188,7 @@ namespace MoonTools.ECS.Rev2 // add the new component to the new archetype var columnIndex = nextArchetype.ComponentToColumnIndex[componentId]; - var column = nextArchetype.Components[columnIndex]; + var column = nextArchetype.ComponentColumns[columnIndex]; column.Append(component); } @@ -207,9 +230,9 @@ namespace MoonTools.ECS.Rev2 var archetype = record.Archetype; var row = record.Row; - for (int i = 0; i < archetype.Components.Count; i += 1) + for (int i = 0; i < archetype.ComponentColumns.Count; i += 1) { - archetype.Components[i].Delete(row); + archetype.ComponentColumns[i].Delete(row); } if (row != archetype.Count - 1) @@ -227,16 +250,16 @@ namespace MoonTools.ECS.Rev2 private void MoveEntityToHigherArchetype(EntityId entityId, int row, Archetype from, Archetype to) { - for (int i = 0; i < from.Components.Count; i += 1) + for (int i = 0; i < from.ComponentColumns.Count; i += 1) { var componentId = from.Signature[i]; var destinationColumnIndex = to.ComponentToColumnIndex[componentId]; // copy all components to higher archetype - from.Components[i].CopyToEnd(row, to.Components[destinationColumnIndex]); + from.ComponentColumns[i].CopyElementToEnd(row, to.ComponentColumns[destinationColumnIndex]); // delete row on from archetype - from.Components[i].Delete(row); + from.ComponentColumns[i].Delete(row); } if (row != from.Count - 1) @@ -256,18 +279,18 @@ namespace MoonTools.ECS.Rev2 private void MoveEntityToLowerArchetype(EntityId entityId, int row, Archetype from, Archetype to, ComponentId removed) { - for (int i = 0; i < from.Components.Count; i += 1) + for (int i = 0; i < from.ComponentColumns.Count; i += 1) { var componentId = from.Signature[i]; // delete the row - from.Components[i].Delete(row); + from.ComponentColumns[i].Delete(row); // if this isn't the removed component, copy to the lower archetype if (componentId != removed) { var destinationColumnIndex = to.ComponentToColumnIndex[componentId]; - from.Components[i].CopyToEnd(row, to.Components[destinationColumnIndex]); + from.ComponentColumns[i].CopyElementToEnd(row, to.ComponentColumns[destinationColumnIndex]); } } @@ -293,11 +316,11 @@ namespace MoonTools.ECS.Rev2 { var componentIdOne = archetype.Signature[0]; var columnIndexOne = archetype.ComponentToColumnIndex[componentIdOne]; - var columnOneElements = archetype.Components[columnIndexOne].Elements; + var columnOneElements = archetype.ComponentColumns[columnIndexOne].Elements; var componentIdTwo = archetype.Signature[1]; var columnIndexTwo = archetype.ComponentToColumnIndex[componentIdTwo]; - var columnTwoElements = archetype.Components[columnIndexTwo].Elements; + var columnTwoElements = archetype.ComponentColumns[columnIndexTwo].Elements; for (int i = archetype.Count - 1; i >= 0; i -= 1) { @@ -312,11 +335,11 @@ namespace MoonTools.ECS.Rev2 { var componentIdOne = archetype.Signature[0]; var columnIndexOne = archetype.ComponentToColumnIndex[componentIdOne]; - var columnOneElements = archetype.Components[columnIndexOne].Elements; + var columnOneElements = archetype.ComponentColumns[columnIndexOne].Elements; var componentIdTwo = archetype.Signature[1]; var columnIndexTwo = archetype.ComponentToColumnIndex[componentIdTwo]; - var columnTwoElements = archetype.Components[columnIndexTwo].Elements; + var columnTwoElements = archetype.ComponentColumns[columnIndexTwo].Elements; for (int i = archetype.Count - 1; i >= 0; i -= 1) { @@ -336,7 +359,7 @@ namespace MoonTools.ECS.Rev2 { for (var i = 0; i < archetype.Signature.Count; i += 1) { - archetype.Components[i].Dispose(); + archetype.ComponentColumns[i].Dispose(); } } }