From f69d132a5e7b526cba88d2ed77fe3d773df904d2 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 10 Jan 2023 00:41:00 +0000 Subject: [PATCH] Storage refactor, snapshot system, experimental template system (#3) - Major storage refactor to improve performance and reduce garbage collection - Snapshot system to facilitate rollback implementation - Experimental template system to instantiate entities based on a template. Use at your own risk, this may change significantly or be removed! Reviewed-on: https://gitea.moonside.games/MoonsideGames/MoonTools.ECS/pulls/3 --- src/ComponentDepot.cs | 272 ++++------------------- src/ComponentStorage.cs | 86 +++---- src/DebugSystem.cs | 20 +- src/DynamicArray.cs | 40 ++++ src/Entity.cs | 10 + src/EntityComponentReader.cs | 36 +-- src/EntityStorage.cs | 72 ++++-- src/Enumerators/ReverseSpanEnumerator.cs | 36 +++ src/Filter.cs | 16 +- src/FilterBuilder.cs | 33 +-- src/FilterSignature.cs | 35 +-- src/FilterStorage.cs | 146 ++++++++++++ src/IHasEntity.cs | 7 - src/IndexableSet.cs | 67 +++--- src/MessageDepot.cs | 21 +- src/MessageStorage.cs | 46 ++-- src/Random.cs | 42 +++- src/Relation.cs | 37 --- src/RelationDepot.cs | 111 ++++++--- src/RelationStorage.cs | 254 +++++++++++---------- src/Snapshot.cs | 122 ++++++++++ src/State/ComponentDepotState.cs | 11 - src/State/ComponentStorageState.cs | 27 --- src/State/EntityStorageState.cs | 10 - src/State/IndexableSetState.cs | 16 -- src/State/RelationDepotState.cs | 10 - src/State/RelationStorageState.cs | 27 --- src/State/WorldState.cs | 16 -- src/System.cs | 65 +++--- src/Template.cs | 13 ++ src/TemplateStorage.cs | 34 +++ src/TypeIndices.cs | 33 +++ src/World.cs | 104 ++++++--- 33 files changed, 1072 insertions(+), 803 deletions(-) create mode 100644 src/DynamicArray.cs create mode 100644 src/Enumerators/ReverseSpanEnumerator.cs create mode 100644 src/FilterStorage.cs delete mode 100644 src/IHasEntity.cs delete mode 100644 src/Relation.cs create mode 100644 src/Snapshot.cs delete mode 100644 src/State/ComponentDepotState.cs delete mode 100644 src/State/ComponentStorageState.cs delete mode 100644 src/State/EntityStorageState.cs delete mode 100644 src/State/IndexableSetState.cs delete mode 100644 src/State/RelationDepotState.cs delete mode 100644 src/State/RelationStorageState.cs delete mode 100644 src/State/WorldState.cs create mode 100644 src/Template.cs create mode 100644 src/TemplateStorage.cs create mode 100644 src/TypeIndices.cs diff --git a/src/ComponentDepot.cs b/src/ComponentDepot.cs index c9c84f1..6f746fa 100644 --- a/src/ComponentDepot.cs +++ b/src/ComponentDepot.cs @@ -6,41 +6,36 @@ namespace MoonTools.ECS { internal class ComponentDepot { - private Dictionary storages = new Dictionary(); + private TypeIndices ComponentTypeIndices; - private Dictionary> filterSignatureToEntityIDs = new Dictionary>(); + private ComponentStorage[] storages = new ComponentStorage[256]; - private Dictionary> typeToFilterSignatures = new Dictionary>(); - -#if DEBUG - private Dictionary singleComponentFilters = new Dictionary(); -#endif - - private HashSet TypesWithDisabledSerialization = new HashSet(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Register() where TComponent : unmanaged + public ComponentDepot(TypeIndices componentTypeIndices) { - if (!storages.ContainsKey(typeof(TComponent))) - { - storages.Add(typeof(TComponent), new ComponentStorage()); -#if DEBUG - singleComponentFilters.Add(typeof(TComponent), CreateFilter(new HashSet() { typeof(TComponent) }, new HashSet())); -#endif - } + ComponentTypeIndices = componentTypeIndices; } - private ComponentStorage Lookup(Type type) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Register(int index) where TComponent : unmanaged { - return storages[type]; + if (index >= storages.Length) + { + Array.Resize(ref storages, storages.Length * 2); + } + + storages[index] = new ComponentStorage(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ComponentStorage Lookup() where TComponent : unmanaged { - // TODO: is it possible to optimize this? - Register(); - return (ComponentStorage) storages[typeof(TComponent)]; + var storageIndex = ComponentTypeIndices.GetIndex(); + // TODO: is there some way to avoid this null check? + if (storages[storageIndex] == null) + { + Register(storageIndex); + } + return (ComponentStorage) storages[storageIndex]; } public bool Some() where TComponent : unmanaged @@ -48,41 +43,19 @@ namespace MoonTools.ECS return Lookup().Any(); } - public bool Has(int entityID) where TComponent : unmanaged - { - return Lookup().Has(entityID); - } - - private bool Has(Type type, int entityID) - { - return Lookup(type).Has(entityID); - } - public ref readonly TComponent Get(int entityID) where TComponent : unmanaged { return ref Lookup().Get(entityID); } - public ref readonly TComponent Get() where TComponent : unmanaged + public ref readonly TComponent GetFirst() where TComponent : unmanaged { - return ref Lookup().Get(); + return ref Lookup().GetFirst(); } public void Set(int entityID, in TComponent component) where TComponent : unmanaged { - var existed = Lookup().Set(entityID, component); - - // update filters - if (!existed) - { - if (typeToFilterSignatures.TryGetValue(typeof(TComponent), out var filterSignatures)) - { - foreach (var filterSignature in filterSignatures) - { - CheckFilter(filterSignature, entityID); - } - } - } + Lookup().Set(entityID, component); } public Entity GetSingletonEntity() where TComponent : unmanaged @@ -95,208 +68,61 @@ namespace MoonTools.ECS return Lookup().AllComponents(); } - private void Remove(Type type, int entityID) + public void Remove(int entityID, int storageIndex) { - var existed = Lookup(type).Remove(entityID); - - // update filters - if (existed) - { - if (typeToFilterSignatures.TryGetValue(type, out var filterSignatures)) - { - foreach (var filterSignature in filterSignatures) - { - CheckFilter(filterSignature, entityID); - } - } - } + storages[storageIndex].Remove(entityID); } public void Remove(int entityID) where TComponent : unmanaged { - var existed = Lookup().Remove(entityID); + Lookup().Remove(entityID); + } - // update filters - if (existed) + public void Clear() + { + for (var i = 0; i < ComponentTypeIndices.Count; i += 1) { - if (typeToFilterSignatures.TryGetValue(typeof(TComponent), out var filterSignatures)) + if (storages[i] != null) { - foreach (var filterSignature in filterSignatures) - { - CheckFilter(filterSignature, entityID); - } + storages[i].Clear(); } } } - // TODO: is there some way to optimize this without complicating serialization? - public void OnEntityDestroy(int entityID) + // these methods used to implement snapshots, templates, and debugging + + internal unsafe void* UntypedGet(int entityID, int componentTypeIndex) { - foreach (var type in storages.Keys) - { - Remove(type, entityID); - } + return storages[componentTypeIndex].UntypedGet(entityID); } - public Filter CreateFilter(HashSet included, HashSet excluded) + internal unsafe void Set(int entityID, int componentTypeIndex, void* component) { - var filterSignature = new FilterSignature(included, excluded); - if (!filterSignatureToEntityIDs.ContainsKey(filterSignature)) - { - filterSignatureToEntityIDs.Add(filterSignature, new IndexableSet()); + storages[componentTypeIndex].Set(entityID, component); + } - foreach (var type in included) + public void CreateMissingStorages(ComponentDepot other) + { + for (var i = 0; i < ComponentTypeIndices.Count; i += 1) + { + if (storages[i] == null && other.storages[i] != null) { - if (!typeToFilterSignatures.ContainsKey(type)) - { - typeToFilterSignatures.Add(type, new HashSet()); - } - - typeToFilterSignatures[type].Add(filterSignature); - } - - foreach (var type in excluded) - { - if (!typeToFilterSignatures.ContainsKey(type)) - { - typeToFilterSignatures.Add(type, new HashSet()); - } - - typeToFilterSignatures[type].Add(filterSignature); - } - } - return new Filter(this, included, excluded); - } - - // FIXME: this dictionary should probably just store entities - public IEnumerable FilterEntities(Filter filter) - { - foreach (var id in filterSignatureToEntityIDs[filter.Signature]) - { - yield return new Entity(id); - } - } - - public IEnumerable FilterEntitiesRandom(Filter filter) - { - foreach (var index in RandomGenerator.LinearCongruentialGenerator(FilterCount(filter))) - { - yield return new Entity(filterSignatureToEntityIDs[filter.Signature][index]); - } - } - - public Entity FilterNthEntity(Filter filter, int index) - { - return new Entity(filterSignatureToEntityIDs[filter.Signature][index]); - } - - public Entity FilterRandomEntity(Filter filter) - { - var randomIndex = RandomGenerator.Next(FilterCount(filter)); - return new Entity(filterSignatureToEntityIDs[filter.Signature][randomIndex]); - } - - public int FilterCount(Filter filter) - { - return filterSignatureToEntityIDs[filter.Signature].Count; - } - - private void CheckFilter(FilterSignature filterSignature, int entityID) - { - foreach (var type in filterSignature.Included) - { - if (!Has(type, entityID)) - { - filterSignatureToEntityIDs[filterSignature].Remove(entityID); - return; - } - } - - foreach (var type in filterSignature.Excluded) - { - if (Has(type, entityID)) - { - filterSignatureToEntityIDs[filterSignature].Remove(entityID); - return; - } - } - - filterSignatureToEntityIDs[filterSignature].Add(entityID); - } - - public void DisableSerialization() where TComponent : unmanaged - { - TypesWithDisabledSerialization.Add(typeof(TComponent)); - } - - public void Save(ComponentDepotState state) - { - foreach (var (type, storage) in storages) - { - if (!TypesWithDisabledSerialization.Contains(type)) - { - if (!state.StorageStates.ContainsKey(type)) - { - state.StorageStates.Add(type, storage.CreateState()); - } - - storage.Save(state.StorageStates[type]); - } - } - - foreach (var (signature, set) in filterSignatureToEntityIDs) - { - // FIXME: we could cache this - if (!signature.Included.Overlaps(TypesWithDisabledSerialization) && !signature.Excluded.Overlaps(TypesWithDisabledSerialization)) - { - if (!state.FilterStates.ContainsKey(signature)) - { - state.FilterStates[signature] = new IndexableSetState(set.Count); - } - set.Save(state.FilterStates[signature]); + storages[i] = other.storages[i].CreateStorage(); } } } - public void Load(ComponentDepotState state) - { - foreach (var (type, storageState) in state.StorageStates) - { - storages[type].Load(storageState); - } - - foreach (var (signature, setState) in state.FilterStates) - { - filterSignatureToEntityIDs[signature].Load(setState); - } - } + // this method is used to iterate components of an entity, only for use with a debug inspector #if DEBUG - public IEnumerable Debug_GetAllComponents(int entityID) + public object Debug_Get(int entityID, int componentTypeIndex) { - foreach (var (type, storage) in storages) - { - if (storage.Has(entityID)) - { - yield return storage.Debug_Get(entityID); - } - } + return storages[componentTypeIndex].Debug_Get(entityID); } - public IEnumerable Debug_GetEntities(Type componentType) + public IEnumerable Debug_GetEntityIDs(int componentTypeIndex) { - return singleComponentFilters[componentType].Entities; - } - - public IEnumerable Debug_SearchComponentType(string typeString) - { - foreach (var type in storages.Keys) - { - if (type.ToString().ToLower().Contains(typeString.ToLower())) - { - yield return type; - } - } + return storages[componentTypeIndex].Debug_GetEntityIDs(); } #endif } diff --git a/src/ComponentStorage.cs b/src/ComponentStorage.cs index 8324558..41117bc 100644 --- a/src/ComponentStorage.cs +++ b/src/ComponentStorage.cs @@ -1,17 +1,22 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; namespace MoonTools.ECS { internal abstract class ComponentStorage { - public abstract bool Has(int entityID); + internal abstract unsafe void Set(int entityID, void* component); public abstract bool Remove(int entityID); - public abstract object Debug_Get(int entityID); - public abstract ComponentStorageState CreateState(); - public abstract void Save(ComponentStorageState state); - public abstract void Load(ComponentStorageState state); + public abstract void Clear(); + + // used for debugging and template instantiation + internal abstract unsafe void* UntypedGet(int entityID); + // used to create correctly typed storage on snapshot + public abstract ComponentStorage CreateStorage(); +#if DEBUG + internal abstract object Debug_Get(int entityID); + internal abstract IEnumerable Debug_GetEntityIDs(); +#endif } internal class ComponentStorage : ComponentStorage where TComponent : unmanaged @@ -26,22 +31,20 @@ namespace MoonTools.ECS return nextID > 0; } - public override bool Has(int entityID) - { - return entityIDToStorageIndex.ContainsKey(entityID); - } - public ref readonly TComponent Get(int entityID) { return ref components[entityIDToStorageIndex[entityID]]; } - public override object Debug_Get(int entityID) + internal override unsafe void* UntypedGet(int entityID) { - return components[entityIDToStorageIndex[entityID]]; + fixed (void* p = &components[entityIDToStorageIndex[entityID]]) + { + return p; + } } - public ref readonly TComponent Get() + public ref readonly TComponent GetFirst() { #if DEBUG if (nextID == 0) @@ -52,11 +55,8 @@ namespace MoonTools.ECS return ref components[0]; } - // Returns true if the entity already had this component. - public bool Set(int entityID, in TComponent component) + public void Set(int entityID, in TComponent component) { - bool result = true; - if (!entityIDToStorageIndex.ContainsKey(entityID)) { var index = nextID; @@ -70,21 +70,21 @@ namespace MoonTools.ECS entityIDToStorageIndex[entityID] = index; entityIDs[index] = entityID; - - result = false; } components[entityIDToStorageIndex[entityID]] = component; + } - return result; + internal override unsafe void Set(int entityID, void* component) + { + Set(entityID, *((TComponent*) component)); } // Returns true if the entity had this component. public override bool Remove(int entityID) { - if (entityIDToStorageIndex.ContainsKey(entityID)) + if (entityIDToStorageIndex.TryGetValue(entityID, out int storageIndex)) { - var storageIndex = entityIDToStorageIndex[entityID]; entityIDToStorageIndex.Remove(entityID); var lastElementIndex = nextID - 1; @@ -106,7 +106,7 @@ namespace MoonTools.ECS return false; } - public void Clear() + public override void Clear() { nextID = 0; entityIDToStorageIndex.Clear(); @@ -128,43 +128,21 @@ namespace MoonTools.ECS return new Entity(entityIDs[0]); } - public override ComponentStorageState CreateState() + public override ComponentStorage CreateStorage() { - return ComponentStorageState.Create(nextID); + return new ComponentStorage(); } - public override void Save(ComponentStorageState state) +#if DEBUG + internal override object Debug_Get(int entityID) { - ReadOnlySpan entityIDBytes = MemoryMarshal.Cast(new ReadOnlySpan(entityIDs, 0, nextID)); - - if (entityIDBytes.Length > state.EntityIDs.Length) - { - Array.Resize(ref state.EntityIDs, entityIDBytes.Length); - } - entityIDBytes.CopyTo(state.EntityIDs); - - ReadOnlySpan componentBytes = MemoryMarshal.Cast(AllComponents()); - if (componentBytes.Length > state.Components.Length) - { - Array.Resize(ref state.Components, componentBytes.Length); - } - componentBytes.CopyTo(state.Components); - - state.Count = nextID; + return components[entityIDToStorageIndex[entityID]]; } - public override void Load(ComponentStorageState state) + internal override IEnumerable Debug_GetEntityIDs() { - state.EntityIDs.CopyTo(MemoryMarshal.Cast(entityIDs)); - state.Components.CopyTo(MemoryMarshal.Cast(components)); - - entityIDToStorageIndex.Clear(); - for (var i = 0; i < state.Count; i += 1) - { - entityIDToStorageIndex[entityIDs[i]] = i; - } - - nextID = state.Count; + return entityIDToStorageIndex.Keys; } +#endif } } diff --git a/src/DebugSystem.cs b/src/DebugSystem.cs index 2db79cc..de9027a 100644 --- a/src/DebugSystem.cs +++ b/src/DebugSystem.cs @@ -13,19 +13,31 @@ namespace MoonTools.ECS { } - protected IEnumerable Debug_GetAllComponents(Entity entity) + protected IEnumerable Debug_GetAllComponents(Entity entity) { - return ComponentDepot.Debug_GetAllComponents(entity.ID); + foreach (var typeIndex in EntityStorage.ComponentTypeIndices(entity.ID)) + { + yield return ComponentDepot.Debug_Get(entity.ID, typeIndex); + } } protected IEnumerable Debug_GetEntities(Type componentType) { - return ComponentDepot.Debug_GetEntities(componentType); + foreach (var entityID in ComponentDepot.Debug_GetEntityIDs(ComponentTypeIndices.GetIndex(componentType))) + { + yield return new Entity(entityID); + } } protected IEnumerable Debug_SearchComponentType(string typeString) { - return ComponentDepot.Debug_SearchComponentType(typeString); + foreach (var type in ComponentTypeIndices.Types) + { + if (type.ToString().ToLower().Contains(typeString.ToLower())) + { + yield return type; + } + } } } } diff --git a/src/DynamicArray.cs b/src/DynamicArray.cs new file mode 100644 index 0000000..c16b697 --- /dev/null +++ b/src/DynamicArray.cs @@ -0,0 +1,40 @@ +using System; + +namespace MoonTools.ECS +{ + public class DynamicArray where T : unmanaged + { + private T[] Array; + public int Count { get; private set; } + + public Span ToSpan() => new Span(Array, 0, Count); + public ReverseSpanEnumerator GetEnumerator() => new ReverseSpanEnumerator(new Span(Array, 0, Count)); + + public DynamicArray(int capacity = 16) + { + Array = new T[capacity]; + Count = 0; + } + + public ref T this[int i] + { + get { return ref Array[i]; } + } + + public void Add(T item) + { + if (Count >= Array.Length) + { + global::System.Array.Resize(ref Array, Array.Length * 2); + } + + Array[Count] = item; + Count += 1; + } + + public void Clear() + { + Count = 0; + } + } +} diff --git a/src/Entity.cs b/src/Entity.cs index ba87a1e..e91da6e 100644 --- a/src/Entity.cs +++ b/src/Entity.cs @@ -35,5 +35,15 @@ namespace MoonTools.ECS { return !a.Equals(b); } + + public static implicit operator int(Entity e) + { + return e.ID; + } + + public static implicit operator Entity(int i) + { + return new Entity(i); + } } } diff --git a/src/EntityComponentReader.cs b/src/EntityComponentReader.cs index 81b2c76..e55f6dd 100644 --- a/src/EntityComponentReader.cs +++ b/src/EntityComponentReader.cs @@ -9,7 +9,12 @@ namespace MoonTools.ECS internal EntityStorage EntityStorage => World.EntityStorage; internal ComponentDepot ComponentDepot => World.ComponentDepot; internal RelationDepot RelationDepot => World.RelationDepot; - protected FilterBuilder FilterBuilder => new FilterBuilder(ComponentDepot); + protected FilterBuilder FilterBuilder => new FilterBuilder(FilterStorage, ComponentTypeIndices); + internal FilterStorage FilterStorage => World.FilterStorage; + internal TypeIndices ComponentTypeIndices => World.ComponentTypeIndices; + internal TypeIndices RelationTypeIndices => World.RelationTypeIndices; + internal TemplateStorage TemplateStorage => World.TemplateStorage; + internal ComponentDepot TemplateComponentDepot => World.TemplateComponentDepot; public EntityComponentReader(World world) { @@ -23,7 +28,8 @@ namespace MoonTools.ECS protected bool Has(in Entity entity) where TComponent : unmanaged { - return ComponentDepot.Has(entity.ID); + var storageIndex = ComponentTypeIndices.GetIndex(); + return EntityStorage.HasComponent(entity.ID, storageIndex); } protected bool Some() where TComponent : unmanaged @@ -38,7 +44,7 @@ namespace MoonTools.ECS protected ref readonly TComponent GetSingleton() where TComponent : unmanaged { - return ref ComponentDepot.Get(); + return ref ComponentDepot.GetFirst(); } protected Entity GetSingletonEntity() where TComponent : unmanaged @@ -46,12 +52,7 @@ namespace MoonTools.ECS return ComponentDepot.GetSingletonEntity(); } - protected bool Exists(in Entity entity) - { - return EntityStorage.Exists(entity); - } - - protected IEnumerable<(Entity, Entity, TRelationKind)> Relations() where TRelationKind : unmanaged + protected ReverseSpanEnumerator<(Entity, Entity)> Relations() where TRelationKind : unmanaged { return RelationDepot.Relations(); } @@ -61,13 +62,18 @@ namespace MoonTools.ECS return RelationDepot.Related(a.ID, b.ID); } - // relations go A->B, so given A, will give all outgoing B relations. - protected IEnumerable<(Entity, TRelationKind)> OutRelations(in Entity entity) where TRelationKind : unmanaged + protected TRelationKind GetRelationData(in Entity a, in Entity b) where TRelationKind : unmanaged + { + return RelationDepot.Get(a, b); + } + + // relations go A->B, so given A, will give all entities in outgoing relations of this kind. + protected ReverseSpanEnumerator OutRelations(in Entity entity) where TRelationKind : unmanaged { return RelationDepot.OutRelations(entity.ID); } - protected (Entity, TRelationKind) OutRelationSingleton(in Entity entity) where TRelationKind : unmanaged + protected Entity OutRelationSingleton(in Entity entity) where TRelationKind : unmanaged { return RelationDepot.OutRelationSingleton(entity.ID); } @@ -82,13 +88,13 @@ namespace MoonTools.ECS return RelationDepot.OutRelationCount(entity.ID); } - // Relations go A->B, so given B, will give all incoming A relations. - protected IEnumerable<(Entity, TRelationKind)> InRelations(in Entity entity) where TRelationKind : unmanaged + // Relations go A->B, so given B, will give all entities in incoming A relations of this kind. + protected ReverseSpanEnumerator InRelations(in Entity entity) where TRelationKind : unmanaged { return RelationDepot.InRelations(entity.ID); } - protected (Entity, TRelationKind) InRelationSingleton(in Entity entity) where TRelationKind : unmanaged + protected Entity InRelationSingleton(in Entity entity) where TRelationKind : unmanaged { return RelationDepot.InRelationSingleton(entity.ID); } diff --git a/src/EntityStorage.cs b/src/EntityStorage.cs index 2379b5d..d2f912d 100644 --- a/src/EntityStorage.cs +++ b/src/EntityStorage.cs @@ -5,12 +5,22 @@ namespace MoonTools.ECS internal class EntityStorage { private int nextID = 0; + // FIXME: why is this duplicated? private readonly Stack availableIDs = new Stack(); + // FIXME: this is only needed in debug mode private readonly HashSet availableIDHash = new HashSet(); + private Dictionary> EntityToComponentTypeIndices = new Dictionary>(); + private Dictionary> EntityToRelationTypeIndices = new Dictionary>(); + + public int Count => nextID - availableIDs.Count; + public Entity Create() { - return new Entity(NextID()); + var entity = new Entity(NextID()); + EntityToComponentTypeIndices.TryAdd(entity.ID, new HashSet()); + EntityToRelationTypeIndices.TryAdd(entity.ID, new HashSet()); + return entity; } public bool Exists(in Entity entity) @@ -20,29 +30,61 @@ namespace MoonTools.ECS public void Destroy(in Entity entity) { + EntityToComponentTypeIndices[entity.ID].Clear(); + EntityToRelationTypeIndices[entity.ID].Clear(); Release(entity.ID); } - public void Save(EntityStorageState state) + // Returns true if the component is new. + public bool SetComponent(int entityID, int componentTypeIndex) { - state.NextID = nextID; - state.availableIDs.Clear(); - foreach (var id in availableIDs) - { - state.availableIDs.Add(id); - } + return EntityToComponentTypeIndices[entityID].Add(componentTypeIndex); } - public void Load(EntityStorageState state) + public bool HasComponent(int entityID, int componentTypeIndex) { - nextID = state.NextID; + return EntityToComponentTypeIndices[entityID].Contains(componentTypeIndex); + } + + // Returns true if the component existed. + public bool RemoveComponent(int entityID, int componentTypeIndex) + { + return EntityToComponentTypeIndices[entityID].Remove(componentTypeIndex); + } + + public void AddRelationKind(int entityID, int relationIndex) + { + EntityToRelationTypeIndices[entityID].Add(relationIndex); + } + + public void RemoveRelation(int entityId, int relationIndex) + { + EntityToRelationTypeIndices[entityId].Remove(relationIndex); + } + + public HashSet ComponentTypeIndices(int entityID) + { + return EntityToComponentTypeIndices[entityID]; + } + + public HashSet RelationTypeIndices(int entityID) + { + return EntityToRelationTypeIndices[entityID]; + } + + public void Clear() + { + nextID = 0; + foreach (var componentSet in EntityToComponentTypeIndices.Values) + { + componentSet.Clear(); + } + foreach (var relationSet in EntityToRelationTypeIndices.Values) + { + relationSet.Clear(); + } availableIDs.Clear(); availableIDHash.Clear(); - foreach (var id in state.availableIDs) - { - availableIDs.Push(id); - availableIDHash.Add(id); - } } private int NextID() diff --git a/src/Enumerators/ReverseSpanEnumerator.cs b/src/Enumerators/ReverseSpanEnumerator.cs new file mode 100644 index 0000000..ce526a5 --- /dev/null +++ b/src/Enumerators/ReverseSpanEnumerator.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.CompilerServices; + +namespace MoonTools.ECS +{ + public ref struct ReverseSpanEnumerator + { + private ReadOnlySpan Span; + private int index; + + public ReverseSpanEnumerator GetEnumerator() => this; + + public T Current => Span[index]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (index > 0) + { + index -= 1; + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReverseSpanEnumerator(Span span) + { + Span = span; + index = span.Length; + } + + public static ReverseSpanEnumerator Empty => new ReverseSpanEnumerator(); + } +} diff --git a/src/Filter.cs b/src/Filter.cs index b229728..4124428 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -6,22 +6,22 @@ namespace MoonTools.ECS public class Filter { internal FilterSignature Signature; - private ComponentDepot ComponentDepot; + private FilterStorage FilterStorage; - internal Filter(ComponentDepot componentDepot, HashSet included, HashSet excluded) + internal Filter(FilterStorage filterStorage, HashSet included, HashSet excluded) { - ComponentDepot = componentDepot; + FilterStorage = filterStorage; Signature = new FilterSignature(included, excluded); } - public IEnumerable Entities => ComponentDepot.FilterEntities(this); - public IEnumerable EntitiesInRandomOrder => ComponentDepot.FilterEntitiesRandom(this); - public Entity RandomEntity => ComponentDepot.FilterRandomEntity(this); + public ReverseSpanEnumerator Entities => FilterStorage.FilterEntities(Signature); + public LinearCongruentialEnumerator EntitiesInRandomOrder => FilterStorage.FilterEntitiesRandom(Signature); + public Entity RandomEntity => FilterStorage.FilterRandomEntity(Signature); - public int Count => ComponentDepot.FilterCount(this); + public int Count => FilterStorage.FilterCount(Signature); public bool Empty => Count == 0; // WARNING: this WILL crash if the index is out of range! - public Entity NthEntity(int index) => ComponentDepot.FilterNthEntity(this, index); + public Entity NthEntity(int index) => FilterStorage.FilterNthEntity(Signature, index); } } diff --git a/src/FilterBuilder.cs b/src/FilterBuilder.cs index be75edc..ed1562e 100644 --- a/src/FilterBuilder.cs +++ b/src/FilterBuilder.cs @@ -5,41 +5,42 @@ namespace MoonTools.ECS { public struct FilterBuilder { - private ComponentDepot ComponentDepot; - private HashSet Included; - private HashSet Excluded; + private TypeIndices ComponentTypeIndices; + private FilterStorage FilterStorage; + private HashSet Included; + private HashSet Excluded; - internal FilterBuilder(ComponentDepot componentDepot) + internal FilterBuilder(FilterStorage filterStorage, TypeIndices componentTypeIndices) { - ComponentDepot = componentDepot; - Included = new HashSet(); - Excluded = new HashSet(); + FilterStorage = filterStorage; + ComponentTypeIndices = componentTypeIndices; + Included = new HashSet(); + Excluded = new HashSet(); } - private FilterBuilder(ComponentDepot componentDepot, HashSet included, HashSet excluded) + private FilterBuilder(FilterStorage filterStorage, TypeIndices componentTypeIndices, HashSet included, HashSet excluded) { - ComponentDepot = componentDepot; + FilterStorage = filterStorage; + ComponentTypeIndices = componentTypeIndices; Included = included; Excluded = excluded; } public FilterBuilder Include() where TComponent : unmanaged { - ComponentDepot.Register(); - Included.Add(typeof(TComponent)); - return new FilterBuilder(ComponentDepot, Included, Excluded); + Included.Add(ComponentTypeIndices.GetIndex()); + return new FilterBuilder(FilterStorage, ComponentTypeIndices, Included, Excluded); } public FilterBuilder Exclude() where TComponent : unmanaged { - ComponentDepot.Register(); - Excluded.Add(typeof(TComponent)); - return new FilterBuilder(ComponentDepot, Included, Excluded); + Excluded.Add(ComponentTypeIndices.GetIndex()); + return new FilterBuilder(FilterStorage, ComponentTypeIndices, Included, Excluded); } public Filter Build() { - return ComponentDepot.CreateFilter(Included, Excluded); + return FilterStorage.CreateFilter(Included, Excluded); } } } diff --git a/src/FilterSignature.cs b/src/FilterSignature.cs index 7508b36..8fd65ec 100644 --- a/src/FilterSignature.cs +++ b/src/FilterSignature.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; namespace MoonTools.ECS { - public struct FilterSignature + public struct FilterSignature : IEquatable { - private const int HASH_FACTOR = 97; + public readonly HashSet Included; + public readonly HashSet Excluded; - public readonly HashSet Included; - public readonly HashSet Excluded; - - public FilterSignature(HashSet included, HashSet excluded) + public FilterSignature(HashSet included, HashSet excluded) { Included = included; Excluded = excluded; @@ -26,26 +24,31 @@ namespace MoonTools.ECS return Included.SetEquals(other.Included) && Excluded.SetEquals(other.Excluded); } - private int GuidToInt(Guid guid) - { - return BitConverter.ToInt32(guid.ToByteArray()); - } - public override int GetHashCode() { - int result = 1; + var hashcode = 1; + foreach (var type in Included) { - result *= HASH_FACTOR + GuidToInt(type.GUID); + hashcode = HashCode.Combine(hashcode, type); } - // FIXME: Is there a way to avoid collisions when this is the same set as included? foreach (var type in Excluded) { - result *= HASH_FACTOR + GuidToInt(type.GUID); + hashcode = HashCode.Combine(hashcode, type); } - return result; + return hashcode; + } + + public static bool operator ==(FilterSignature left, FilterSignature right) + { + return left.Equals(right); + } + + public static bool operator !=(FilterSignature left, FilterSignature right) + { + return !(left == right); } } } diff --git a/src/FilterStorage.cs b/src/FilterStorage.cs new file mode 100644 index 0000000..b8ea8f3 --- /dev/null +++ b/src/FilterStorage.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; + +namespace MoonTools.ECS +{ + internal class FilterStorage + { + private EntityStorage EntityStorage; + private TypeIndices ComponentTypeIndices; + private Dictionary> filterSignatureToEntityIDs = new Dictionary>(); + private Dictionary> typeToFilterSignatures = new Dictionary>(); + + public FilterStorage(EntityStorage entityStorage, TypeIndices componentTypeIndices) + { + EntityStorage = entityStorage; + ComponentTypeIndices = componentTypeIndices; + } + + public Filter CreateFilter(HashSet included, HashSet excluded) + { + var filterSignature = new FilterSignature(included, excluded); + if (!filterSignatureToEntityIDs.ContainsKey(filterSignature)) + { + filterSignatureToEntityIDs.Add(filterSignature, new IndexableSet()); + + foreach (var type in included) + { + if (!typeToFilterSignatures.ContainsKey(type)) + { + typeToFilterSignatures.Add(type, new HashSet()); + } + + typeToFilterSignatures[type].Add(filterSignature); + } + + foreach (var type in excluded) + { + if (!typeToFilterSignatures.ContainsKey(type)) + { + typeToFilterSignatures.Add(type, new HashSet()); + } + + typeToFilterSignatures[type].Add(filterSignature); + } + } + return new Filter(this, included, excluded); + } + + public ReverseSpanEnumerator FilterEntities(FilterSignature filterSignature) + { + return filterSignatureToEntityIDs[filterSignature].GetEnumerator(); + } + + public LinearCongruentialEnumerator FilterEntitiesRandom(FilterSignature filterSignature) + { + return RandomGenerator.LinearCongruentialGenerator(FilterCount(filterSignature)); + } + + public Entity FilterNthEntity(FilterSignature filterSignature, int index) + { + return new Entity(filterSignatureToEntityIDs[filterSignature][index]); + } + + public Entity FilterRandomEntity(FilterSignature filterSignature) + { + var randomIndex = RandomGenerator.Next(FilterCount(filterSignature)); + return new Entity(filterSignatureToEntityIDs[filterSignature][randomIndex]); + } + + public int FilterCount(FilterSignature filterSignature) + { + return filterSignatureToEntityIDs[filterSignature].Count; + } + + public void Check(int entityID, int componentTypeIndex) + { + if (typeToFilterSignatures.TryGetValue(componentTypeIndex, out var filterSignatures)) + { + foreach (var filterSignature in filterSignatures) + { + CheckFilter(entityID, filterSignature); + } + } + } + + public void Check(int entityID) where TComponent : unmanaged + { + Check(entityID, ComponentTypeIndices.GetIndex()); + } + + public bool CheckSatisfied(int entityID, FilterSignature filterSignature) + { + foreach (var type in filterSignature.Included) + { + if (!EntityStorage.HasComponent(entityID, type)) + { + return false; + } + } + + foreach (var type in filterSignature.Excluded) + { + if (EntityStorage.HasComponent(entityID, type)) + { + return false; + } + } + + return true; + } + + private void CheckFilter(int entityID, FilterSignature filterSignature) + { + foreach (var type in filterSignature.Included) + { + if (!EntityStorage.HasComponent(entityID, type)) + { + filterSignatureToEntityIDs[filterSignature].Remove(entityID); + return; + } + } + + foreach (var type in filterSignature.Excluded) + { + if (EntityStorage.HasComponent(entityID, type)) + { + filterSignatureToEntityIDs[filterSignature].Remove(entityID); + return; + } + } + + filterSignatureToEntityIDs[filterSignature].Add(entityID); + } + + public void RemoveEntity(int entityID, int componentTypeIndex) + { + if (typeToFilterSignatures.TryGetValue(componentTypeIndex, out var filterSignatures)) + { + foreach (var filterSignature in filterSignatures) + { + filterSignatureToEntityIDs[filterSignature].Remove(entityID); + } + } + } + } +} diff --git a/src/IHasEntity.cs b/src/IHasEntity.cs deleted file mode 100644 index 35a676f..0000000 --- a/src/IHasEntity.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MoonTools.ECS -{ - public interface IHasEntity - { - Entity Entity { get; } - } -} diff --git a/src/IndexableSet.cs b/src/IndexableSet.cs index bacb429..1a79f71 100644 --- a/src/IndexableSet.cs +++ b/src/IndexableSet.cs @@ -1,15 +1,15 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; namespace MoonTools.ECS { - internal class IndexableSet : IEnumerable where T : unmanaged + internal class IndexableSet where T : unmanaged { private Dictionary indices; private T[] array; public int Count { get; private set; } + public ReverseSpanEnumerator GetEnumerator() => new ReverseSpanEnumerator(new Span(array, 0, Count)); public IndexableSet(int size = 32) { @@ -64,52 +64,47 @@ namespace MoonTools.ECS return true; } - public IEnumerator GetEnumerator() - { - for (var i = Count - 1; i >= 0; i -= 1) - { - yield return array[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - for (var i = Count - 1; i >= 0; i -= 1) - { - yield return array[i]; - } - } - public void Clear() { Count = 0; } - public void Save(IndexableSetState state) + public struct Enumerator { - ReadOnlySpan arrayBytes = MemoryMarshal.Cast(array); + /// The set being enumerated. + private readonly IndexableSet _set; + /// The next index to yield. + private int _index; - if (arrayBytes.Length > state.Array.Length) + /// Initialize the enumerator. + /// The set to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(IndexableSet set) { - Array.Resize(ref state.Array, arrayBytes.Length); + _set = set; + _index = _set.Count; } - arrayBytes.CopyTo(state.Array); - - state.Count = Count; - } - - public void Load(IndexableSetState state) - { - state.Array.CopyTo(MemoryMarshal.Cast(array)); - - indices.Clear(); - for (var i = 0; i < state.Count; i += 1) + /// Advances the enumerator to the next element of the span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() { - indices[array[i]] = i; + int index = _index - 1; + if (index >= 0) + { + _index = index; + return true; + } + + return false; } - Count = state.Count; + /// Gets the element at the current position of the enumerator. + public T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _set[_index]; + } } } } diff --git a/src/MessageDepot.cs b/src/MessageDepot.cs index 5f925ff..63b9e2d 100644 --- a/src/MessageDepot.cs +++ b/src/MessageDepot.cs @@ -7,7 +7,7 @@ namespace MoonTools.ECS { private Dictionary storages = new Dictionary(); - private MessageStorage Lookup() where TMessage : struct + private MessageStorage Lookup() where TMessage : unmanaged { if (!storages.ContainsKey(typeof(TMessage))) { @@ -17,37 +17,42 @@ namespace MoonTools.ECS return storages[typeof(TMessage)] as MessageStorage; } - public void Add(in TMessage message) where TMessage : struct + public void Add(in TMessage message) where TMessage : unmanaged { Lookup().Add(message); } - public bool Some() where TMessage : struct + public void Add(int entityID, in TMessage message) where TMessage : unmanaged + { + Lookup().Add(entityID, message); + } + + public bool Some() where TMessage : unmanaged { return Lookup().Some(); } - public ReadOnlySpan All() where TMessage : struct + public ReadOnlySpan All() where TMessage : unmanaged { return Lookup().All(); } - public TMessage First() where TMessage : struct + public TMessage First() where TMessage : unmanaged { return Lookup().First(); } - public IEnumerable WithEntity(int entityID) where TMessage : struct, IHasEntity + public ReverseSpanEnumerator WithEntity(int entityID) where TMessage : unmanaged { return Lookup().WithEntity(entityID); } - public ref readonly TMessage FirstWithEntity(int entityID) where TMessage : struct, IHasEntity + public ref readonly TMessage FirstWithEntity(int entityID) where TMessage : unmanaged { return ref Lookup().FirstWithEntity(entityID); } - public bool SomeWithEntity(int entityID) where TMessage : struct, IHasEntity + public bool SomeWithEntity(int entityID) where TMessage : unmanaged { return Lookup().SomeWithEntity(entityID); } diff --git a/src/MessageStorage.cs b/src/MessageStorage.cs index 0e29a1a..b2f29d0 100644 --- a/src/MessageStorage.cs +++ b/src/MessageStorage.cs @@ -8,12 +8,13 @@ namespace MoonTools.ECS public abstract void Clear(); } - internal class MessageStorage : MessageStorage where TMessage : struct + internal class MessageStorage : MessageStorage where TMessage : unmanaged { private int count = 0; private int capacity = 128; private TMessage[] messages; - private Dictionary> entityToIndices = new Dictionary>(); + // duplicating storage here for fast iteration + private Dictionary> entityToMessages = new Dictionary>(); public MessageStorage() { @@ -29,20 +30,20 @@ namespace MoonTools.ECS } messages[count] = message; - - if (message is IHasEntity entityMessage) - { - if (!entityToIndices.ContainsKey(entityMessage.Entity.ID)) - { - entityToIndices.Add(entityMessage.Entity.ID, new List()); - } - - entityToIndices[entityMessage.Entity.ID].Add(count); - } - count += 1; } + public void Add(int entityID, in TMessage message) + { + if (!entityToMessages.ContainsKey(entityID)) + { + entityToMessages.Add(entityID, new DynamicArray()); + } + entityToMessages[entityID].Add(message); + + Add(message); + } + public bool Some() { return count > 0; @@ -58,31 +59,32 @@ namespace MoonTools.ECS return messages[0]; } - public IEnumerable WithEntity(int entityID) + public ReverseSpanEnumerator WithEntity(int entityID) { - if (entityToIndices.ContainsKey(entityID)) + if (entityToMessages.TryGetValue(entityID, out var messages)) { - foreach (var index in entityToIndices[entityID]) - { - yield return messages[index]; - } + return messages.GetEnumerator(); + } + else + { + return ReverseSpanEnumerator.Empty; } } public ref readonly TMessage FirstWithEntity(int entityID) { - return ref messages[entityToIndices[entityID][0]]; + return ref entityToMessages[entityID][0]; } public bool SomeWithEntity(int entityID) { - return entityToIndices.ContainsKey(entityID) && entityToIndices[entityID].Count > 0; + return entityToMessages.ContainsKey(entityID) && entityToMessages[entityID].Count > 0; } public override void Clear() { count = 0; - foreach (var set in entityToIndices.Values) + foreach (var set in entityToMessages.Values) { set.Clear(); } diff --git a/src/Random.cs b/src/Random.cs index 97c0ddd..324eccd 100644 --- a/src/Random.cs +++ b/src/Random.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace MoonTools.ECS { @@ -20,7 +20,7 @@ namespace MoonTools.ECS /// /// A psuedorandom nonrepeating sequence of integers from 0 to n. /// - public static IEnumerable LinearCongruentialGenerator(int n) + public static LinearCongruentialEnumerator LinearCongruentialGenerator(int n) { var x = Primes[random.Next(Primes.Length - 1)]; while (x % n == 0) @@ -29,12 +29,44 @@ namespace MoonTools.ECS x = Primes[random.Next(Primes.Length - 1)]; } - var start = random.Next(n); + return new LinearCongruentialEnumerator(random.Next(n), x, n); + } + } - for (var i = start; i < start + n; i++) + public struct LinearCongruentialEnumerator + { + private readonly int start; + private readonly int count; + private readonly int prime; + private int current; + + public LinearCongruentialEnumerator GetEnumerator() => this; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal LinearCongruentialEnumerator(int start, int prime, int count) + { + current = start; + this.start = start; + this.prime = prime; + this.count = count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + current += 1; + if (current < start + count) { - yield return (i * x) % n; + return true; } + + return false; + } + + public int Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (current * prime) % count; } } } diff --git a/src/Relation.cs b/src/Relation.cs deleted file mode 100644 index 165aca6..0000000 --- a/src/Relation.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace MoonTools.ECS -{ - internal struct Relation : IEquatable - { - public Entity A { get; } - public Entity B { get; } - - internal Relation(Entity entityA, Entity entityB) - { - A = entityA; - B = entityB; - } - - internal Relation(int idA, int idB) - { - A = new Entity(idA); - B = new Entity(idB); - } - - public override bool Equals(object? obj) - { - return obj is Relation relation && Equals(relation); - } - - public bool Equals(Relation other) - { - return A.ID == other.A.ID && B.ID == other.B.ID; - } - - public override int GetHashCode() - { - return HashCode.Combine(A.ID, B.ID); - } - } -} diff --git a/src/RelationDepot.cs b/src/RelationDepot.cs index 4b8652c..06bb2fe 100644 --- a/src/RelationDepot.cs +++ b/src/RelationDepot.cs @@ -6,31 +6,49 @@ namespace MoonTools.ECS { internal class RelationDepot { - private Dictionary storages = new Dictionary(); + private TypeIndices RelationTypeIndices; + private RelationStorage[] storages = new RelationStorage[256]; - private void Register() where TRelationKind : unmanaged + public RelationDepot(TypeIndices relationTypeIndices) { - if (!storages.ContainsKey(typeof(TRelationKind))) + RelationTypeIndices = relationTypeIndices; + } + + private void Register(int index) where TRelationKind : unmanaged + { + if (index >= storages.Length) { - storages.Add(typeof(TRelationKind), new RelationStorage()); + Array.Resize(ref storages, storages.Length * 2); } + + storages[index] = new RelationStorage(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private RelationStorage Lookup() where TRelationKind : unmanaged { - Register(); - return (RelationStorage) storages[typeof(TRelationKind)]; + var storageIndex = RelationTypeIndices.GetIndex(); + // TODO: is there some way to avoid this null check? + if (storages[storageIndex] == null) + { + Register(storageIndex); + } + return (RelationStorage) storages[storageIndex]; } - public void Set(Relation relation, TRelationKind relationData) where TRelationKind : unmanaged + public void Set(in Entity entityA, in Entity entityB, TRelationKind relationData) where TRelationKind : unmanaged { - Lookup().Set(relation, relationData); + Lookup().Set(entityA, entityB, relationData); } - public void Remove(Relation relation) where TRelationKind : unmanaged + public TRelationKind Get(in Entity entityA, in Entity entityB) where TRelationKind : unmanaged { - Lookup().Remove(relation); + return Lookup().Get(entityA, entityB); + } + + public (bool, bool) Remove(in Entity entityA, in Entity entityB) where TRelationKind : unmanaged + { + return Lookup().Remove(entityA, entityB); } public void UnrelateAll(int entityID) where TRelationKind : unmanaged @@ -38,31 +56,22 @@ namespace MoonTools.ECS Lookup().UnrelateAll(entityID); } - // FIXME: optimize this - public void OnEntityDestroy(int entityID) - { - foreach (var storage in storages.Values) - { - storage.OnEntityDestroy(entityID); - } - } - - public IEnumerable<(Entity, Entity, TRelationKind)> Relations() where TRelationKind : unmanaged + public ReverseSpanEnumerator<(Entity, Entity)> Relations() where TRelationKind : unmanaged { return Lookup().All(); } public bool Related(int idA, int idB) where TRelationKind : unmanaged { - return Lookup().Has(new Relation(idA, idB)); + return Lookup().Has((idA, idB)); } - public IEnumerable<(Entity, TRelationKind)> OutRelations(int entityID) where TRelationKind : unmanaged + public ReverseSpanEnumerator OutRelations(int entityID) where TRelationKind : unmanaged { return Lookup().OutRelations(entityID); } - public (Entity, TRelationKind) OutRelationSingleton(int entityID) where TRelationKind : unmanaged + public Entity OutRelationSingleton(int entityID) where TRelationKind : unmanaged { return Lookup().OutFirst(entityID); } @@ -77,12 +86,12 @@ namespace MoonTools.ECS return Lookup().HasOutRelation(entityID); } - public IEnumerable<(Entity, TRelationKind)> InRelations(int entityID) where TRelationKind : unmanaged + public ReverseSpanEnumerator InRelations(int entityID) where TRelationKind : unmanaged { return Lookup().InRelations(entityID); } - public (Entity, TRelationKind) InRelationSingleton(int entityID) where TRelationKind : unmanaged + public Entity InRelationSingleton(int entityID) where TRelationKind : unmanaged { return Lookup().InFirst(entityID); } @@ -97,24 +106,52 @@ namespace MoonTools.ECS return Lookup().InRelationCount(entityID); } - public void Save(RelationDepotState state) - { - foreach (var (type, storage) in storages) - { - if (!state.StorageStates.ContainsKey(type)) - { - state.StorageStates.Add(type, storage.CreateState()); - } + // untyped methods used for destroying and snapshots - storage.Save(state.StorageStates[type]); + public unsafe void Set(int entityA, int entityB, int relationTypeIndex, void* relationData) + { + storages[relationTypeIndex].Set(entityA, entityB, relationData); + } + + public int GetStorageIndex(int relationTypeIndex, int entityA, int entityB) + { + return storages[relationTypeIndex].GetStorageIndex(entityA, entityB); + } + + public unsafe void* Get(int relationTypeIndex, int relationStorageIndex) + { + return storages[relationTypeIndex].Get(relationStorageIndex); + } + + public void UnrelateAll(int entityID, int relationTypeIndex) + { + storages[relationTypeIndex].UnrelateAll(entityID); + } + + public ReverseSpanEnumerator OutRelations(int entityID, int relationTypeIndex) + { + return storages[relationTypeIndex].OutRelations(entityID); + } + + public void Clear() + { + for (var i = 0; i < RelationTypeIndices.Count; i += 1) + { + if (storages[i] != null) + { + storages[i].Clear(); + } } } - public void Load(RelationDepotState state) + public void CreateMissingStorages(RelationDepot other) { - foreach (var (type, storageState) in state.StorageStates) + for (var i = 0; i < RelationTypeIndices.Count; i += 1) { - storages[type].Load(storageState); + if (storages[i] == null && other.storages[i] != null) + { + storages[i] = other.storages[i].CreateStorage(); + } } } } diff --git a/src/RelationStorage.cs b/src/RelationStorage.cs index bf86f9d..a069768 100644 --- a/src/RelationStorage.cs +++ b/src/RelationStorage.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; namespace MoonTools.ECS { internal abstract class RelationStorage { - public abstract RelationStorageState CreateState(); - public abstract void Save(RelationStorageState state); - public abstract void Load(RelationStorageState state); - public abstract void OnEntityDestroy(int entityID); + public abstract unsafe void Set(int entityA, int entityB, void* relationData); + public abstract int GetStorageIndex(int entityA, int entityB); + public abstract unsafe void* Get(int relationStorageIndex); + public abstract void UnrelateAll(int entityID); + public abstract ReverseSpanEnumerator OutRelations(int entityID); + public abstract RelationStorage CreateStorage(); + public abstract void Clear(); } // Relation is the two entities, A related to B. @@ -17,33 +19,30 @@ namespace MoonTools.ECS internal class RelationStorage : RelationStorage where TRelation : unmanaged { private int count = 0; - private Dictionary indices = new Dictionary(16); - private Relation[] relations = new Relation[16]; + private Dictionary<(Entity, Entity), int> indices = new Dictionary<(Entity, Entity), int>(16); + private (Entity, Entity)[] relations = new (Entity, Entity)[16]; private TRelation[] relationDatas = new TRelation[16]; - private Dictionary> outRelations = new Dictionary>(16); - private Dictionary> inRelations = new Dictionary>(16); - private Stack> listPool = new Stack>(); + private Dictionary> outRelations = new Dictionary>(16); + private Dictionary> inRelations = new Dictionary>(16); + private Stack> listPool = new Stack>(); - public IEnumerable<(Entity, Entity, TRelation)> All() + public ReverseSpanEnumerator<(Entity, Entity)> All() { - for (var i = 0; i < count; i += 1) - { - var relation = relations[i]; - yield return (relation.A, relation.B, relationDatas[i]); - } + return new ReverseSpanEnumerator<(Entity, Entity)>(new Span<(Entity, Entity)>(relations, 0, count)); } - public void Set(Relation relation, TRelation relationData) + public void Set(in Entity entityA, in Entity entityB, TRelation relationData) { - if (indices.ContainsKey(relation)) + var relation = (entityA, entityB); + + if (indices.TryGetValue(relation, out var index)) { - var index = indices[relation]; relationDatas[index] = relationData; return; } - var idA = relation.A.ID; - var idB = relation.B.ID; + var idA = entityA.ID; + var idB = entityB.ID; if (!outRelations.ContainsKey(idA)) { @@ -69,34 +68,37 @@ namespace MoonTools.ECS count += 1; } - public bool Has(Relation relation) + public TRelation Get(in Entity entityA, in Entity entityB) + { + return relationDatas[indices[(entityA, entityB)]]; + } + + public bool Has((Entity, Entity) relation) { return indices.ContainsKey(relation); } - // FIXME: creating the new Relation in here is slightly deranged - public IEnumerable<(Entity, TRelation)> OutRelations(int entityID) + public override ReverseSpanEnumerator OutRelations(int entityID) { - if (outRelations.ContainsKey(entityID)) + if (outRelations.TryGetValue(entityID, out var entityOutRelations)) { - foreach (var id in outRelations[entityID]) - { - var relation = new Relation(entityID, id); - yield return (relation.B, relationDatas[indices[relation]]); - } + return entityOutRelations.GetEnumerator(); + } + else + { + return ReverseSpanEnumerator.Empty; } } - public (Entity, TRelation) OutFirst(int entityID) + public Entity OutFirst(int entityID) { #if DEBUG - if (!outRelations.ContainsKey(entityID)) + if (!outRelations.ContainsKey(entityID) || outRelations[entityID].Count == 0) { throw new KeyNotFoundException("No out relations to this entity!"); } #endif - var relation = new Relation(entityID, outRelations[entityID][0]); - return (relation.B, relationDatas[indices[relation]]); + return outRelations[entityID][0]; } public bool HasOutRelation(int entityID) @@ -106,32 +108,31 @@ namespace MoonTools.ECS public int OutRelationCount(int entityID) { - return outRelations.ContainsKey(entityID) ? outRelations[entityID].Count : 0; + return outRelations.TryGetValue(entityID, out var entityOutRelations) ? entityOutRelations.Count : 0; } - public IEnumerable<(Entity, TRelation)> InRelations(int entityID) + public ReverseSpanEnumerator InRelations(int entityID) { - if (inRelations.ContainsKey(entityID)) + if (inRelations.TryGetValue(entityID, out var entityInRelations)) { - foreach (var id in inRelations[entityID]) - { - var relation = new Relation(id, entityID); - yield return (relation.A, relationDatas[indices[relation]]); - } + return entityInRelations.GetEnumerator(); + } + else + { + return ReverseSpanEnumerator.Empty; } } - public (Entity, TRelation) InFirst(int entityID) + public Entity InFirst(int entityID) { #if DEBUG - if (!inRelations.ContainsKey(entityID)) + if (!inRelations.ContainsKey(entityID) || inRelations[entityID].Count == 0) { throw new KeyNotFoundException("No out relations to this entity!"); } #endif - var relation = new Relation(inRelations[entityID][0], entityID); - return (relation.A, relationDatas[indices[relation]]); + return inRelations[entityID][0]; } public bool HasInRelation(int entityID) @@ -141,24 +142,35 @@ namespace MoonTools.ECS public int InRelationCount(int entityID) { - return inRelations.ContainsKey(entityID) ? inRelations[entityID].Count : 0; + return inRelations.TryGetValue(entityID, out var entityInRelations) ? entityInRelations.Count : 0; } - public bool Remove(Relation relation) + public (bool, bool) Remove(in Entity entityA, in Entity entityB) { - if (outRelations.ContainsKey(relation.A.ID)) + var aEmpty = false; + var bEmpty = false; + var relation = (entityA, entityB); + + if (outRelations.TryGetValue(entityA.ID, out var entityOutRelations)) { - outRelations[relation.A.ID].Remove(relation.B.ID); + entityOutRelations.Remove(entityB.ID); + if (outRelations[entityA.ID].Count == 0) + { + aEmpty = true; + } } - if (inRelations.ContainsKey(relation.B.ID)) + if (inRelations.TryGetValue(entityB.ID, out var entityInRelations)) { - inRelations[relation.B.ID].Remove(relation.A.ID); + entityInRelations.Remove(entityA.ID); + if (inRelations[entityB.ID].Count == 0) + { + bEmpty = true; + } } - if (indices.ContainsKey(relation)) + if (indices.TryGetValue(relation, out var index)) { - var index = indices[relation]; var lastElementIndex = count - 1; // move an element into the hole @@ -172,111 +184,93 @@ namespace MoonTools.ECS count -= 1; indices.Remove(relation); - return true; } - return false; + return (aEmpty, bEmpty); } - public void UnrelateAll(int entityID) - { - if (outRelations.ContainsKey(entityID)) - { - foreach (var entityB in outRelations[entityID]) - { - Remove(new Relation(entityID, entityB)); - } - - ReturnHashSetToPool(outRelations[entityID]); - outRelations.Remove(entityID); - } - - if (inRelations.ContainsKey(entityID)) - { - foreach (var entityA in inRelations[entityID]) - { - Remove(new Relation(entityA, entityID)); - } - - ReturnHashSetToPool(inRelations[entityID]); - inRelations.Remove(entityID); - } - } - - public override void OnEntityDestroy(int entityID) - { - UnrelateAll(entityID); - } - - private IndexableSet AcquireHashSetFromPool() + private IndexableSet AcquireHashSetFromPool() { if (listPool.Count == 0) { - listPool.Push(new IndexableSet()); + listPool.Push(new IndexableSet()); } return listPool.Pop(); } - private void ReturnHashSetToPool(IndexableSet hashSet) + private void ReturnHashSetToPool(IndexableSet hashSet) { hashSet.Clear(); listPool.Push(hashSet); } - public override RelationStorageState CreateState() + // untyped methods used for internal implementation + + public override unsafe void Set(int entityA, int entityB, void* relationData) { - return RelationStorageState.Create(count); + Set(entityA, entityB, *((TRelation*) relationData)); } - public override void Save(RelationStorageState state) + public override int GetStorageIndex(int entityA, int entityB) { - ReadOnlySpan relationBytes = MemoryMarshal.Cast(relations); - - if (relationBytes.Length > state.Relations.Length) - { - Array.Resize(ref state.Relations, relationBytes.Length); - } - relationBytes.CopyTo(state.Relations); - - ReadOnlySpan relationDataBytes = MemoryMarshal.Cast(relationDatas); - - if (relationDataBytes.Length > state.RelationDatas.Length) - { - Array.Resize(ref state.RelationDatas, relationDataBytes.Length); - } - relationDataBytes.CopyTo(state.RelationDatas); - - state.Count = count; + return indices[(entityA, entityB)]; } - public override void Load(RelationStorageState state) + public override unsafe void* Get(int relationStorageIndex) { - state.Relations.CopyTo(MemoryMarshal.Cast(relations)); - state.RelationDatas.CopyTo(MemoryMarshal.Cast(relationDatas)); + fixed (void* p = &relations[relationStorageIndex]) + { + return p; + } + } + public override void UnrelateAll(int entityID) + { + if (outRelations.TryGetValue(entityID, out var entityOutRelations)) + { + foreach (var entityB in entityOutRelations) + { + Remove(entityID, entityB); + } + + ReturnHashSetToPool(entityOutRelations); + outRelations.Remove(entityID); + } + + if (inRelations.TryGetValue(entityID, out var entityInRelations)) + { + foreach (var entityA in entityInRelations) + { + Remove(entityA, entityID); + } + + ReturnHashSetToPool(entityInRelations); + inRelations.Remove(entityID); + } + } + + public override RelationStorage CreateStorage() + { + return new RelationStorage(); + } + + public override void Clear() + { + count = 0; indices.Clear(); - outRelations.Clear(); - inRelations.Clear(); - for (var i = 0; i < state.Count; i += 1) + + foreach (var set in inRelations.Values) { - var relation = relations[i]; - indices[relation] = i; - - if (!outRelations.ContainsKey(relation.A.ID)) - { - outRelations[relation.A.ID] = AcquireHashSetFromPool(); - } - outRelations[relation.A.ID].Add(relation.B.ID); - - if (!inRelations.ContainsKey(relation.B.ID)) - { - inRelations[relation.B.ID] = AcquireHashSetFromPool(); - } - inRelations[relation.B.ID].Add(relation.A.ID); + ReturnHashSetToPool(set); } + inRelations.Clear(); - count = state.Count; + foreach (var set in outRelations.Values) + { + ReturnHashSetToPool(set); + } + outRelations.Clear(); } } } diff --git a/src/Snapshot.cs b/src/Snapshot.cs new file mode 100644 index 0000000..6fd6bcb --- /dev/null +++ b/src/Snapshot.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; + +namespace MoonTools.ECS +{ + public class Snapshot + { + private World World; + private Filter? Filter; + + private EntityStorage SnapshotEntityStorage; + private ComponentDepot SnapshotComponentDepot; + private RelationDepot SnapshotRelationDepot; + + private List SnapshotToWorldID = new List(); + private Dictionary WorldToSnapshotID = new Dictionary(); + + internal Snapshot(World world) + { + World = world; + SnapshotEntityStorage = new EntityStorage(); + SnapshotComponentDepot = new ComponentDepot(World.ComponentTypeIndices); + SnapshotRelationDepot = new RelationDepot(World.RelationTypeIndices); + } + + public unsafe void Take(Filter filter) + { + Clear(); + Filter = filter; + SnapshotComponentDepot.CreateMissingStorages(World.ComponentDepot); + SnapshotRelationDepot.CreateMissingStorages(World.RelationDepot); + + foreach (var worldEntity in filter.Entities) + { + var snapshotEntity = SnapshotEntityStorage.Create(); + WorldToSnapshotID.Add(worldEntity.ID, snapshotEntity.ID); + + foreach (var componentTypeIndex in World.EntityStorage.ComponentTypeIndices(worldEntity.ID)) + { + SnapshotEntityStorage.SetComponent(snapshotEntity.ID, componentTypeIndex); + SnapshotComponentDepot.Set(snapshotEntity.ID, componentTypeIndex, World.ComponentDepot.UntypedGet(worldEntity.ID, componentTypeIndex)); + } + } + + foreach (var worldEntity in filter.Entities) + { + var snapshotEntityID = WorldToSnapshotID[worldEntity.ID]; + + foreach (var relationTypeIndex in World.EntityStorage.RelationTypeIndices(worldEntity.ID)) + { + SnapshotEntityStorage.AddRelationKind(snapshotEntityID, relationTypeIndex); + + foreach (var otherEntityID in World.RelationDepot.OutRelations(worldEntity.ID, relationTypeIndex)) + { +#if DEBUG + if (!World.FilterStorage.CheckSatisfied(otherEntityID, Filter.Signature)) + { + throw new InvalidOperationException($"Snapshot entity {worldEntity.ID} is related to non-snapshot entity {otherEntityID}!"); + } +#endif + var relationStorageIndex = World.RelationDepot.GetStorageIndex(relationTypeIndex, worldEntity, otherEntityID); + var otherSnapshotID = WorldToSnapshotID[otherEntityID]; + SnapshotEntityStorage.AddRelationKind(otherSnapshotID, relationTypeIndex); + SnapshotRelationDepot.Set(snapshotEntityID, otherSnapshotID, relationTypeIndex, World.RelationDepot.Get(relationTypeIndex, relationStorageIndex)); + } + } + } + } + + public unsafe void Restore() + { + if (Filter == null) + { + return; + } + + foreach (var entity in Filter.Entities) + { + World.Destroy(entity); + } + + for (var i = 0; i < SnapshotEntityStorage.Count; i += 1) + { + var entity = World.CreateEntity(); + SnapshotToWorldID.Add(entity.ID); + + foreach (var componentTypeIndex in SnapshotEntityStorage.ComponentTypeIndices(i)) + { + World.EntityStorage.SetComponent(entity.ID, componentTypeIndex); + World.FilterStorage.Check(entity.ID, componentTypeIndex); + World.ComponentDepot.Set(entity.ID, componentTypeIndex, SnapshotComponentDepot.UntypedGet(i, componentTypeIndex)); + } + } + + for (var i = 0; i < SnapshotEntityStorage.Count; i += 1) + { + var worldID = SnapshotToWorldID[i]; + + foreach (var relationTypeIndex in SnapshotEntityStorage.RelationTypeIndices(i)) + { + World.EntityStorage.AddRelationKind(worldID, relationTypeIndex); + + foreach (var otherEntityID in SnapshotRelationDepot.OutRelations(i, relationTypeIndex)) + { + var relationStorageIndex = SnapshotRelationDepot.GetStorageIndex(relationTypeIndex, i, otherEntityID); + var otherEntityWorldID = SnapshotToWorldID[otherEntityID]; + World.RelationDepot.Set(worldID, otherEntityWorldID, relationTypeIndex, SnapshotRelationDepot.Get(relationTypeIndex, relationStorageIndex)); + } + } + } + } + + private void Clear() + { + SnapshotEntityStorage.Clear(); + SnapshotComponentDepot.Clear(); + SnapshotRelationDepot.Clear(); + SnapshotToWorldID.Clear(); + WorldToSnapshotID.Clear(); + } + } +} diff --git a/src/State/ComponentDepotState.cs b/src/State/ComponentDepotState.cs deleted file mode 100644 index 2370dc6..0000000 --- a/src/State/ComponentDepotState.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MoonTools.ECS -{ - internal class ComponentDepotState - { - public Dictionary StorageStates = new Dictionary(); - public Dictionary> FilterStates = new Dictionary>(); - } -} diff --git a/src/State/ComponentStorageState.cs b/src/State/ComponentStorageState.cs deleted file mode 100644 index ad4872d..0000000 --- a/src/State/ComponentStorageState.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; - -namespace MoonTools.ECS -{ - internal class ComponentStorageState - { - public int Count; - public byte[] EntityIDs; - public byte[] Components; - - public unsafe static ComponentStorageState Create(int count) where TComponent : unmanaged - { - return new ComponentStorageState( - count, - count * sizeof(int), - count * sizeof(TComponent) - ); - } - - private ComponentStorageState(int count, int entityIDSize, int componentSize) - { - Count = count; - EntityIDs = new byte[entityIDSize]; - Components = new byte[componentSize]; - } - } -} diff --git a/src/State/EntityStorageState.cs b/src/State/EntityStorageState.cs deleted file mode 100644 index 66363dc..0000000 --- a/src/State/EntityStorageState.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace MoonTools.ECS -{ - internal class EntityStorageState - { - public int NextID; - public List availableIDs = new List(); - } -} diff --git a/src/State/IndexableSetState.cs b/src/State/IndexableSetState.cs deleted file mode 100644 index 4367252..0000000 --- a/src/State/IndexableSetState.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace MoonTools.ECS -{ - internal class IndexableSetState where T : unmanaged - { - public int Count; - public byte[] Array; - - public unsafe IndexableSetState(int count) - { - Count = count; - Array = new byte[sizeof(T) * count]; - } - } -} diff --git a/src/State/RelationDepotState.cs b/src/State/RelationDepotState.cs deleted file mode 100644 index 58e1315..0000000 --- a/src/State/RelationDepotState.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MoonTools.ECS -{ - internal class RelationDepotState - { - public Dictionary StorageStates = new Dictionary(); - } -} diff --git a/src/State/RelationStorageState.cs b/src/State/RelationStorageState.cs deleted file mode 100644 index d210a5d..0000000 --- a/src/State/RelationStorageState.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; - -namespace MoonTools.ECS -{ - internal class RelationStorageState - { - public int Count; - public byte[] Relations; - public byte[] RelationDatas; - - public unsafe static RelationStorageState Create(int count) where TRelation : unmanaged - { - return new RelationStorageState( - count, - count * sizeof(Relation), - count * sizeof(TRelation) - ); - } - - private RelationStorageState(int count, int relationSize, int relationDataSize) - { - Count = count; - Relations = new byte[relationSize]; - RelationDatas = new byte[relationDataSize]; - } - } -} diff --git a/src/State/WorldState.cs b/src/State/WorldState.cs deleted file mode 100644 index d422745..0000000 --- a/src/State/WorldState.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MoonTools.ECS -{ - public class WorldState - { - internal readonly ComponentDepotState ComponentDepotState; - internal readonly EntityStorageState EntityStorageState; - internal readonly RelationDepotState RelationDepotState; - - public WorldState() - { - ComponentDepotState = new ComponentDepotState(); - EntityStorageState = new EntityStorageState(); - RelationDepotState = new RelationDepotState(); - } - } -} diff --git a/src/System.cs b/src/System.cs index 1a993ec..6d1d4dd 100644 --- a/src/System.cs +++ b/src/System.cs @@ -11,28 +11,24 @@ namespace MoonTools.ECS public abstract void Update(TimeSpan delta); - protected Entity CreateEntity() - { - return EntityStorage.Create(); - } + protected Entity CreateEntity() => World.CreateEntity(); - protected void Set(in Entity entity, in TComponent component) where TComponent : unmanaged - { -#if DEBUG - // check for use after destroy - if (!Exists(entity)) - { - throw new ArgumentException("This entity is not valid!"); - } -#endif - ComponentDepot.Set(entity.ID, component); - } + protected void Set(in Entity entity, in TComponent component) where TComponent : unmanaged => World.Set(entity, component); protected void Remove(in Entity entity) where TComponent : unmanaged { - ComponentDepot.Remove(entity.ID); + if (EntityStorage.RemoveComponent(entity.ID, ComponentTypeIndices.GetIndex())) + { + ComponentDepot.Remove(entity.ID); + FilterStorage.Check(entity.ID); + } } + protected void Set(in Template template, in TComponent component) where TComponent : unmanaged => World.Set(template, component); + + // This feature is EXPERIMENTAL. USe at your own risk!! + protected Entity Instantiate(in Template template) => World.Instantiate(template); + protected ReadOnlySpan ReadMessages() where TMessage : unmanaged { return MessageDepot.All(); @@ -48,17 +44,17 @@ namespace MoonTools.ECS return MessageDepot.Some(); } - protected IEnumerable ReadMessagesWithEntity(in Entity entity) where TMessage : unmanaged, IHasEntity + protected ReverseSpanEnumerator ReadMessagesWithEntity(in Entity entity) where TMessage : unmanaged { return MessageDepot.WithEntity(entity.ID); } - protected ref readonly TMessage ReadMessageWithEntity(in Entity entity) where TMessage : unmanaged, IHasEntity + protected ref readonly TMessage ReadMessageWithEntity(in Entity entity) where TMessage : unmanaged { return ref MessageDepot.FirstWithEntity(entity.ID); } - protected bool SomeMessageWithEntity(in Entity entity) where TMessage : unmanaged, IHasEntity + protected bool SomeMessageWithEntity(in Entity entity) where TMessage : unmanaged { return MessageDepot.SomeWithEntity(entity.ID); } @@ -68,27 +64,40 @@ namespace MoonTools.ECS MessageDepot.Add(message); } + protected void Send(in Entity entity, in TMessage message) where TMessage : unmanaged + { + MessageDepot.Add(entity.ID, message); + } + protected void Relate(in Entity entityA, in Entity entityB, TRelationKind relationData) where TRelationKind : unmanaged { - RelationDepot.Set(new Relation(entityA, entityB), relationData); + RelationDepot.Set(entityA, entityB, relationData); + var relationTypeIndex = RelationTypeIndices.GetIndex(); + EntityStorage.AddRelationKind(entityA.ID, relationTypeIndex); + EntityStorage.AddRelationKind(entityB.ID, relationTypeIndex); } protected void Unrelate(in Entity entityA, in Entity entityB) where TRelationKind : unmanaged { - RelationDepot.Remove(new Relation(entityA, entityB)); + var (aEmpty, bEmpty) = RelationDepot.Remove(entityA, entityB); + + if (aEmpty) + { + EntityStorage.RemoveRelation(entityA.ID, RelationTypeIndices.GetIndex()); + } + + if (bEmpty) + { + EntityStorage.RemoveRelation(entityB.ID, RelationTypeIndices.GetIndex()); + } } protected void UnrelateAll(in Entity entity) where TRelationKind : unmanaged { RelationDepot.UnrelateAll(entity.ID); + EntityStorage.RemoveRelation(entity.ID, RelationTypeIndices.GetIndex()); } - // FIXME: this is insanely inefficient - protected void Destroy(in Entity entity) - { - ComponentDepot.OnEntityDestroy(entity.ID); - RelationDepot.OnEntityDestroy(entity.ID); - EntityStorage.Destroy(entity); - } + protected void Destroy(in Entity entity) => World.Destroy(entity); } } diff --git a/src/Template.cs b/src/Template.cs new file mode 100644 index 0000000..7654b7a --- /dev/null +++ b/src/Template.cs @@ -0,0 +1,13 @@ +namespace MoonTools.ECS +{ + // This feature is EXPERIMENTAL. Use at your own risk!! + public struct Template + { + public int ID { get; } + + internal Template(int id) + { + ID = id; + } + } +} diff --git a/src/TemplateStorage.cs b/src/TemplateStorage.cs new file mode 100644 index 0000000..fb9dfa0 --- /dev/null +++ b/src/TemplateStorage.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace MoonTools.ECS +{ + public class TemplateStorage + { + private int nextID = 0; + + private Dictionary> TemplateToComponentTypeIndices = new Dictionary>(); + + public Template Create() + { + TemplateToComponentTypeIndices.Add(nextID, new HashSet()); + return new Template(NextID()); + } + + public bool SetComponent(int templateID, int componentTypeIndex) + { + return TemplateToComponentTypeIndices[templateID].Add(componentTypeIndex); + } + + public HashSet ComponentTypeIndices(int templateID) + { + return TemplateToComponentTypeIndices[templateID]; + } + + private int NextID() + { + var id = nextID; + nextID += 1; + return id; + } + } +} diff --git a/src/TypeIndices.cs b/src/TypeIndices.cs new file mode 100644 index 0000000..2f43850 --- /dev/null +++ b/src/TypeIndices.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace MoonTools.ECS +{ + public class TypeIndices + { + Dictionary TypeToIndex = new Dictionary(); + int nextID = 0; + public int Count => TypeToIndex.Count; + + public int GetIndex() where T : unmanaged + { + if (!TypeToIndex.ContainsKey(typeof(T))) + { + TypeToIndex.Add(typeof(T), nextID); + nextID += 1; + } + + return TypeToIndex[typeof(T)]; + } + + public int GetIndex(Type type) + { + return TypeToIndex[type]; + } + + +#if DEBUG + public Dictionary.KeyCollection Types => TypeToIndex.Keys; +#endif + } +} diff --git a/src/World.cs b/src/World.cs index 939a9bd..17f487f 100644 --- a/src/World.cs +++ b/src/World.cs @@ -1,11 +1,28 @@ -namespace MoonTools.ECS +using System; + +namespace MoonTools.ECS { public class World { + internal readonly TypeIndices ComponentTypeIndices = new TypeIndices(); + internal readonly TypeIndices RelationTypeIndices = new TypeIndices(); internal readonly EntityStorage EntityStorage = new EntityStorage(); - internal readonly ComponentDepot ComponentDepot = new ComponentDepot(); + internal readonly ComponentDepot ComponentDepot; internal readonly MessageDepot MessageDepot = new MessageDepot(); - internal readonly RelationDepot RelationDepot = new RelationDepot(); + internal readonly RelationDepot RelationDepot; + internal readonly FilterStorage FilterStorage; + public FilterBuilder FilterBuilder => new FilterBuilder(FilterStorage, ComponentTypeIndices); + + internal readonly TemplateStorage TemplateStorage = new TemplateStorage(); + internal readonly ComponentDepot TemplateComponentDepot; + + public World() + { + ComponentDepot = new ComponentDepot(ComponentTypeIndices); + RelationDepot = new RelationDepot(RelationTypeIndices); + FilterStorage = new FilterStorage(EntityStorage, ComponentTypeIndices); + TemplateComponentDepot = new ComponentDepot(ComponentTypeIndices); + } public Entity CreateEntity() { @@ -14,7 +31,46 @@ public void Set(Entity entity, in TComponent component) where TComponent : unmanaged { - ComponentDepot.Set(entity.ID, component); +#if DEBUG + // check for use after destroy + if (!EntityStorage.Exists(entity)) + { + throw new InvalidOperationException("This entity is not valid!"); + } +#endif + if (EntityStorage.SetComponent(entity.ID, ComponentTypeIndices.GetIndex())) + { + FilterStorage.Check(entity.ID); + } + + ComponentDepot.Set(entity.ID, component); + } + + public Template CreateTemplate() + { + return TemplateStorage.Create(); + } + + public void Set(in Template template, in TComponent component) where TComponent : unmanaged + { + var componentTypeIndex = ComponentTypeIndices.GetIndex(); + TemplateStorage.SetComponent(template.ID, componentTypeIndex); + TemplateComponentDepot.Set(template.ID, component); + ComponentDepot.Register(componentTypeIndex); + } + + public unsafe Entity Instantiate(in Template template) + { + var entity = EntityStorage.Create(); + + foreach (var componentTypeIndex in TemplateStorage.ComponentTypeIndices(template.ID)) + { + EntityStorage.SetComponent(entity.ID, componentTypeIndex); + FilterStorage.Check(entity.ID, componentTypeIndex); + ComponentDepot.Set(entity.ID, componentTypeIndex, TemplateComponentDepot.UntypedGet(template.ID, componentTypeIndex)); + } + + return entity; } public void Send(in TMessage message) where TMessage : unmanaged @@ -22,33 +78,31 @@ MessageDepot.Add(message); } + public void Destroy(in Entity entity) + { + foreach (var componentTypeIndex in EntityStorage.ComponentTypeIndices(entity.ID)) + { + ComponentDepot.Remove(entity.ID, componentTypeIndex); + FilterStorage.RemoveEntity(entity.ID, componentTypeIndex); + } + + foreach (var relationTypeIndex in EntityStorage.RelationTypeIndices(entity.ID)) + { + RelationDepot.UnrelateAll(entity.ID, relationTypeIndex); + } + + EntityStorage.Destroy(entity); + } + + public void FinishUpdate() { MessageDepot.Clear(); } - public void DisableSerialization() where TComponent : unmanaged + public Snapshot CreateSnapshot() { - ComponentDepot.DisableSerialization(); - } - - public WorldState CreateState() - { - return new WorldState(); - } - - public void Save(WorldState state) - { - ComponentDepot.Save(state.ComponentDepotState); - EntityStorage.Save(state.EntityStorageState); - RelationDepot.Save(state.RelationDepotState); - } - - public void Load(WorldState state) - { - ComponentDepot.Load(state.ComponentDepotState); - EntityStorage.Load(state.EntityStorageState); - RelationDepot.Load(state.RelationDepotState); + return new Snapshot(this); } } }