diff --git a/Svelto.ECS/EnginesRoot.Engines.cs b/Svelto.ECS/EnginesRoot.Engines.cs index 70c8266..a3c3754 100644 --- a/Svelto.ECS/EnginesRoot.Engines.cs +++ b/Svelto.ECS/EnginesRoot.Engines.cs @@ -50,9 +50,11 @@ public EnginesRoot(IEntitiesSubmissionScheduler entitiesComponentScheduler) new FasterDictionary, ITypeSafeDictionary>>(); _groupsPerEntity = new FasterDictionary, FasterDictionary>(); _groupedEntityToAdd = new DoubleBufferedEntitiesToAdd(); + _entityLocatorMap = new FasterList(); + _egidToLocatorMap = new FasterDictionary>(); _entitiesStream = new EntitiesStream(); - _entitiesDB = new EntitiesDB(_groupEntityComponentsDB, _groupsPerEntity, _entitiesStream); + _entitiesDB = new EntitiesDB(_groupEntityComponentsDB, _groupsPerEntity, _entitiesStream, new EntityLocatorMap(this)); scheduler = entitiesComponentScheduler; scheduler.onTick = new EntitiesSubmitter(this); @@ -88,7 +90,7 @@ public void Dispose() Svelto.Console.LogException(e); } } - + foreach (FasterDictionary, ITypeSafeDictionary>>. KeyValuePairFast groups in _groupEntityComponentsDB) { @@ -104,7 +106,7 @@ public void Dispose() Svelto.Console.LogException(e); } } - + foreach (FasterDictionary, ITypeSafeDictionary>>. KeyValuePairFast groups in _groupEntityComponentsDB) { @@ -129,7 +131,7 @@ public void Dispose() #endif _groupedEntityToAdd.Dispose(); _entitiesStream.Dispose(); - + scheduler.Dispose(); } diff --git a/Svelto.ECS/EnginesRoot.Entities.cs b/Svelto.ECS/EnginesRoot.Entities.cs index f25e6a3..fe53f07 100644 --- a/Svelto.ECS/EnginesRoot.Entities.cs +++ b/Svelto.ECS/EnginesRoot.Entities.cs @@ -26,6 +26,7 @@ EntityComponentInitializer BuildEntity { CheckAddEntityID(entityID, implementorType); Check.Require(entityID.groupID != 0, "invalid group detected"); + CreateLocator(entityID); var dic = EntityFactory.BuildGroupedEntities(entityID, _groupedEntityToAdd, componentsToBuild , implementors); @@ -70,6 +71,16 @@ void MoveEntityFromAndToEngines(IComponentBuilder[] componentBuilders, EGID from { var fromGroup = GetGroup(fromEntityGID.groupID); + // Update the egid to unique id maps. + if (toEntityGID.HasValue) + { + UpdateLocator(fromEntityGID, toEntityGID.Value); + } + else + { + RemoveLocator(fromEntityGID); + } + //Check if there is an EntityInfoView linked to this entity, if so it's a DynamicEntityDescriptor! if (fromGroup.TryGetValue(new RefWrapper(ComponentBuilderUtilities.ENTITY_STRUCT_INFO_VIEW) , out var entityInfoViewDic) @@ -192,6 +203,8 @@ void SwapEntitiesBetweenGroups(uint fromIdGroupId, uint toGroupId, in PlatformPr FasterDictionary, ITypeSafeDictionary> fromGroup = GetGroup(fromIdGroupId); FasterDictionary, ITypeSafeDictionary> toGroup = GetOrCreateGroup(toGroupId, profiler); + UpdateAllGroupLocators(fromIdGroupId, toGroupId); + foreach (FasterDictionary, ITypeSafeDictionary>.KeyValuePairFast dictionaryOfEntities in fromGroup) { @@ -273,6 +286,8 @@ void RemoveGroupAndEntities(uint groupID, in PlatformProfiler profiler) FasterDictionary, ITypeSafeDictionary> dictionariesOfEntities = _groupEntityComponentsDB[groupID]; + RemoveAllGroupLocators(groupID); + foreach (FasterDictionary, ITypeSafeDictionary>.KeyValuePairFast dictionaryOfEntities in dictionariesOfEntities) { dictionaryOfEntities.Value.RemoveEntitiesFromEngines(_reactiveEnginesAddRemove, profiler diff --git a/Svelto.ECS/EnginesRoot.LocatorMap.cs b/Svelto.ECS/EnginesRoot.LocatorMap.cs new file mode 100644 index 0000000..4f2b7a5 --- /dev/null +++ b/Svelto.ECS/EnginesRoot.LocatorMap.cs @@ -0,0 +1,215 @@ +using System.Collections.Specialized; +using System.Runtime.CompilerServices; +using Svelto.DataStructures; + +namespace Svelto.ECS +{ + public partial class EnginesRoot + { + // The EntityLocatorMap provides a bidirectional map to help locate entities without using an EGID which might + // change in runtime. The Entity Locator map uses a reusable unique identifier struct called EntityLocator to + // find the last known EGID from last entity submission. + class EntityLocatorMap : IEntityLocatorMap + { + public EntityLocatorMap(EnginesRoot enginesRoot) + { + _enginesRoot = new WeakReference(enginesRoot); + } + + public EntityLocator GetLocator(EGID egid) + { + return _enginesRoot.Target.GetLocator(egid); + } + + public bool TryGetEGID(EntityLocator locator, out EGID egid) + { + return _enginesRoot.Target.TryGetEGID(locator, out egid); + } + + WeakReference _enginesRoot; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void CreateLocator(EGID egid) + { + // Check if we need to create a new EntityLocator or whether we can recycle an existing one.s + EntityLocator locator; + if (_nextEntityId == _entityLocatorMap.count) + { + _entityLocatorMap.Add(new EntityLocatorMapElement(egid)); + locator = new EntityLocator(_nextEntityId++); + } + else + { + ref var element = ref _entityLocatorMap[_nextEntityId]; + locator = new EntityLocator(_nextEntityId, element.version); + // The recycle entities form a linked list, using the egid.entityID to store the next element. + _nextEntityId = element.egid.entityID; + element.egid = egid; + } + + // When we create a new one there is nothing to recycle anymore, so we need to update the last recycle entityId. + if (_nextEntityId == _entityLocatorMap.count) + { + _lastEntityId = _entityLocatorMap.count; + } + + // Update reverse map from egid to locator. + if (_egidToLocatorMap.TryGetValue(egid.groupID, out var groupMap) == false) + { + groupMap = new FasterDictionary(); + _egidToLocatorMap[egid.groupID] = groupMap; + } + groupMap[egid.entityID] = locator; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void UpdateLocator(EGID from, EGID to) + { + var locator = GetAndRemoveLocator(from); + +#if DEBUG && !PROFILE_SVELTO + if (locator.Equals(EntityLocator.Invalid)) + { + throw new ECSException("Unable to update locator from egid: " + .FastConcat(from.ToString(), "to egid: ") + .FastConcat(to.ToString(), ". Locator was not found") + ); + } +#endif + + _entityLocatorMap[locator.uniqueID].egid = to; + + if (_egidToLocatorMap.TryGetValue(to.groupID, out var groupMap) == false) + { + groupMap = new FasterDictionary(); + _egidToLocatorMap[to.groupID] = groupMap; + } + groupMap[to.entityID] = locator; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void RemoveLocator(EGID egid) + { + var locator = GetAndRemoveLocator(egid); + +#if DEBUG && !PROFILE_SVELTO + if (locator.Equals(EntityLocator.Invalid)) + { + throw new ECSException("Unable to remove locator for egid: " + .FastConcat(egid.ToString(), ". Locator was not found") + ); + } +#endif + + // Check if this is the first recycled element. + if (_lastEntityId == _entityLocatorMap.count) + { + _nextEntityId = locator.uniqueID; + } + // Otherwise add it as the last recycled element. + else + { + _entityLocatorMap[_lastEntityId].egid = new EGID(locator.uniqueID, 0); + } + + // Invalidate the entity locator element by bumping its version and setting the egid to point to a unexisting element. + _entityLocatorMap[locator.uniqueID].egid = new EGID(_entityLocatorMap.count, 0); + _entityLocatorMap[locator.uniqueID].version++; + + // Mark the element as the last element used. + _lastEntityId = locator.uniqueID; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void RemoveAllGroupLocators(uint groupId) + { + if (_egidToLocatorMap.TryGetValue(groupId, out var groupMap) == false) + { + return; + } + + // We need to traverse all entities in the group and remove the locator using the egid. + // RemoveLocator would modify the enumerator so this is why we traverse the dictionary from last to first. + var keys = groupMap.unsafeKeys; + for (var i = groupMap.count - 1; true; i--) + { + RemoveLocator(new EGID(keys[i].key, groupId)); + if (i == 0) break; + } + + _egidToLocatorMap.Remove(groupId); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void UpdateAllGroupLocators(uint fromGroupId, uint toGroupId) + { + if (_egidToLocatorMap.TryGetValue(fromGroupId, out var groupMap) == false) + { + return; + } + + // We need to traverse all entities in the group and update the locator using the egid. + // UpdateLocator would modify the enumerator so this is why we traverse the dictionary from last to first. + var keys = groupMap.unsafeKeys; + for (var i = groupMap.count - 1; true; i--) + { + UpdateLocator(new EGID(keys[i].key, fromGroupId), new EGID(keys[i].key, toGroupId)); + if (i == 0) break; + } + + _egidToLocatorMap.Remove(fromGroupId); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + EntityLocator GetLocator(EGID egid) + { + if (_egidToLocatorMap.TryGetValue(egid.groupID, out var groupMap)) + { + if (groupMap.TryGetValue(egid.entityID, out var locator)) + { + return locator; + } + } + + return EntityLocator.Invalid; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + EntityLocator GetAndRemoveLocator(EGID egid) + { + if (_egidToLocatorMap.TryGetValue(egid.groupID, out var groupMap)) + { + if (groupMap.TryGetValue(egid.entityID, out var locator)) + { + groupMap.Remove(egid.entityID); + return locator; + } + } + + return EntityLocator.Invalid; + } + + bool TryGetEGID(EntityLocator locator, out EGID egid) + { + egid = new EGID(); + if (locator == EntityLocator.Invalid) return false; + // Make sure we are querying for the current version of the locator. + // Otherwise the locator is pointing to a removed entity. + if (_entityLocatorMap[locator.uniqueID].version == locator.version) + { + egid = _entityLocatorMap[locator.uniqueID].egid; + return true; + } + else + { + return false; + } + } + + uint _nextEntityId; + uint _lastEntityId; + readonly FasterList _entityLocatorMap; + readonly FasterDictionary> _egidToLocatorMap; + } +} \ No newline at end of file diff --git a/Svelto.ECS/EntitiesDB.cs b/Svelto.ECS/EntitiesDB.cs index 781f4dd..24da167 100644 --- a/Svelto.ECS/EntitiesDB.cs +++ b/Svelto.ECS/EntitiesDB.cs @@ -16,11 +16,25 @@ public partial class EntitiesDB internal EntitiesDB (FasterDictionary, ITypeSafeDictionary>> groupEntityComponentsDB , FasterDictionary, FasterDictionary> groupsPerEntity - , EntitiesStream entityStream) + , EntitiesStream entityStream + , IEntityLocatorMap entityLocatorMap) { _groupEntityComponentsDB = groupEntityComponentsDB; _groupsPerEntity = groupsPerEntity; _entityStream = entityStream; + _entityLocatorMap = entityLocatorMap; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool FindEGID(EntityLocator entityLocator, out EGID egid) + { + return _entityLocatorMap.TryGetEGID(entityLocator, out egid); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public EntityLocator GetLocator(EGID egid) + { + return _entityLocatorMap.GetLocator(egid); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -75,7 +89,7 @@ public EntityCollection QueryEntities(ExclusiveGroupStruct group .FastConcat("'. Entity 2: ' count: ".FastConcat(T2entities.count) .FastConcat(" ", typeof(T2).ToString()) .FastConcat("' group: ", groupStruct.ToName()))); -#endif +#endif return new EntityCollection(T1entities, T2entities); } @@ -96,16 +110,16 @@ public EntityCollection QueryEntities(ExclusiveGroupStru .FastConcat(T2entities.count) .FastConcat(" Entity 3: ".FastConcat(typeof(T3).ToString())) .FastConcat(" count: ").FastConcat(T3entities.count))); -#endif +#endif return new EntityCollection(T1entities, T2entities, T3entities); } - + public int IterateOverGroupsAndCount (in LocalFasterReadOnlyList groups) where T : struct, IEntityComponent { int count = 0; - + for (int i = 0; i < groups.count; i++) { count += Count(groups[i]); @@ -154,7 +168,7 @@ public EGIDMapper QueryMappedEntities(ExclusiveGroupStruct groupStructId) return (typeSafeDictionary as ITypeSafeDictionary).ToEGIDMapper(groupStructId); } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryQueryMappedEntities (ExclusiveGroupStruct groupStructId, out EGIDMapper mapper) where T : struct, IEntityComponent @@ -210,7 +224,7 @@ public int Count(ExclusiveGroupStruct groupStruct) where T : struct, IEntityC { if (SafeQueryEntityDictionary(groupStruct, out var typeSafeDictionary) == false) return 0; - + return (int) typeSafeDictionary.count; } @@ -324,5 +338,7 @@ readonly FasterDictionary, ITypeSafeDict //needed to be able to track in which groups a specific entity type can be found. //may change in future as it could be expanded to support queries readonly FasterDictionary, FasterDictionary> _groupsPerEntity; + + readonly IEntityLocatorMap _entityLocatorMap; } } \ No newline at end of file diff --git a/Svelto.ECS/EntityLocator.cs b/Svelto.ECS/EntityLocator.cs new file mode 100644 index 0000000..a1946c9 --- /dev/null +++ b/Svelto.ECS/EntityLocator.cs @@ -0,0 +1,56 @@ +using System; +using System.Runtime.InteropServices; + +#pragma warning disable 660,661 + +namespace Svelto.ECS +{ + [Serialization.DoNotSerialize] + [Serializable] + [StructLayout(LayoutKind.Explicit)] + public struct EntityLocator : IEquatable + { + [FieldOffset(0)] public readonly uint uniqueID; + [FieldOffset(4)] public readonly uint version; + [FieldOffset(0)] readonly ulong _GID; + + public static bool operator ==(EntityLocator obj1, EntityLocator obj2) + { + return obj1._GID == obj2._GID; + } + + public static bool operator !=(EntityLocator obj1, EntityLocator obj2) + { + return obj1._GID != obj2._GID; + } + + public EntityLocator(uint uniqueId) : this(uniqueId, 0) {} + + public EntityLocator(uint uniqueId, uint version) : this() + { + _GID = MAKE_GLOBAL_ID(uniqueId, version); + } + + static ulong MAKE_GLOBAL_ID(uint uniqueId, uint version) + { + return (ulong)version << 32 | ((ulong)uniqueId & 0xFFFFFFFF); + } + + public static EntityLocator Invalid => new EntityLocator(uint.MaxValue, uint.MaxValue); + + public bool Equals(EntityLocator other) + { + return _GID == other._GID; + } + + public bool Equals(EntityLocator x, EntityLocator y) + { + return x == y; + } + + public override string ToString() + { + return "id:".FastConcat(uniqueID).FastConcat(" version:").FastConcat(version); + } + } +} diff --git a/Svelto.ECS/EntityLocatorMapElement.cs b/Svelto.ECS/EntityLocatorMapElement.cs new file mode 100644 index 0000000..16581f5 --- /dev/null +++ b/Svelto.ECS/EntityLocatorMapElement.cs @@ -0,0 +1,16 @@ +using System; + +namespace Svelto.ECS +{ + internal struct EntityLocatorMapElement + { + internal EGID egid; + internal uint version; + + internal EntityLocatorMapElement(EGID egid) + { + this.egid = egid; + version = 0; + } + } +} \ No newline at end of file diff --git a/Svelto.ECS/IEntityLocatorMap.cs b/Svelto.ECS/IEntityLocatorMap.cs new file mode 100644 index 0000000..91687f2 --- /dev/null +++ b/Svelto.ECS/IEntityLocatorMap.cs @@ -0,0 +1,9 @@ +namespace Svelto.ECS +{ + public interface IEntityLocatorMap + { + EntityLocator GetLocator(EGID egid); + + bool TryGetEGID(EntityLocator locator, out EGID egid); + } +} \ No newline at end of file