diff --git a/Ghost.Entities.Test/ArcEntityTest.cs b/Ghost.Entities.Test/ArcEntityTest.cs index ee574f7..d378400 100644 --- a/Ghost.Entities.Test/ArcEntityTest.cs +++ b/Ghost.Entities.Test/ArcEntityTest.cs @@ -15,13 +15,23 @@ public partial class ArcEntityTest : ITest public void Run() { var entity1 = _world.EntityManager.CreateEntity(ComponentTypeID.value); - Console.WriteLine($"{entity1} Has Transform: {_world.EntityManager.HasComponent(entity1)}"); var mesh = new MeshData { index = 1 }; _world.EntityManager.AddComponent(entity1, ref mesh); - Console.WriteLine($"{entity1} Has Mesh: {_world.EntityManager.HasComponent(entity1)}"); - _world.EntityManager.DestoryEntity(entity1); - Console.WriteLine($"{entity1} Has Transform: {_world.EntityManager.HasComponent(entity1)}"); + var queryID = new QueryBuilder().WithAll().Build(_world); + ref var query = ref _world.GetEntityQueryReference(queryID); + + foreach (var item in query) + { + var transforms = item.GetComponentData(); + Console.WriteLine($"Item Count: {item.Count}"); + for (var i = 0; i < item.Count; i++) + { + Console.WriteLine($"Entity Position: {transforms[i].position}"); + transforms[i].position = new float3(1, 2, 3); + Console.WriteLine($"Updated Entity Position: {transforms[i].position}"); + } + } } public void Cleanup() diff --git a/Ghost.Entities/Archetype.cs b/Ghost.Entities/Archetype.cs index 5856782..a034fb3 100644 --- a/Ghost.Entities/Archetype.cs +++ b/Ghost.Entities/Archetype.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; namespace Ghost.Entities; -internal unsafe struct Chuck : IDisposable +internal unsafe struct Chunk : IDisposable { public const int CHUNK_SIZE = 16384; // 16 KB @@ -22,7 +22,7 @@ internal unsafe struct Chuck : IDisposable public int Capacity => _capacity; - public Chuck(int size, int capacity) + public Chunk(int size, int capacity) { _data = new UnsafeArray(size, Allocator.Persistent); _capacity = capacity; @@ -59,11 +59,11 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable private readonly Identifier _id; private readonly Identifier _worldID; - private UnsafeBitSet _signature; - private UnsafeList _chunks; - private UnsafeArray _layouts; - private UnsafeArray _componentIDToOffset; + internal UnsafeBitSet _signature; + internal UnsafeList _chunks; + internal UnsafeArray _layouts; + private UnsafeArray _componentIDToOffset; // TODO: Is hash map better? private UnsafeList _edgesAdd; private UnsafeList _edgesRemove; @@ -75,10 +75,6 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable public Identifier ID => _id; - public UnsafeBitSet Signature => _signature; - public UnsafeList Chunks => _chunks; - public UnsafeArray Layouts => _layouts; - public int EntityCapacity => _entityCapacity; public int ChunkCount => _chunks.Count; @@ -90,14 +86,14 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable if (componentIds.IsEmpty) { _signature = new UnsafeBitSet(1, Allocator.Persistent, AllocationOption.Clear); - _chunks = new UnsafeList(4, Allocator.Persistent); + _chunks = new UnsafeList(4, Allocator.Persistent); _edgesAdd = new UnsafeList(4, Allocator.Persistent); _edgesRemove = new UnsafeList(4, Allocator.Persistent); _signature.ClearAll(); - _entityCapacity = Chuck.CHUNK_SIZE / sizeof(Entity); + _entityCapacity = Chunk.CHUNK_SIZE / sizeof(Entity); return; } @@ -112,7 +108,7 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable } _signature = new UnsafeBitSet(highestComponentID + 1, Allocator.Persistent, AllocationOption.Clear); - _chunks = new UnsafeList(4, Allocator.Persistent); + _chunks = new UnsafeList(4, Allocator.Persistent); _edgesAdd = new UnsafeList(4, Allocator.Persistent); _edgesRemove = new UnsafeList(4, Allocator.Persistent); @@ -148,7 +144,7 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable } _maxComponentID = maxComponentID; - _entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity; + _entityCapacity = Chunk.CHUNK_SIZE / bytesPerEntity; _layouts = new UnsafeArray(components.Length, Allocator.Persistent); _componentIDToOffset = new UnsafeArray(_maxComponentID + 1, Allocator.Persistent); @@ -176,7 +172,7 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable tempOffsets[i] = currentOffset; currentOffset += _entityCapacity * size; - if (currentOffset > Chuck.CHUNK_SIZE) + if (currentOffset > Chunk.CHUNK_SIZE) { fits = false; break; @@ -220,7 +216,7 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable } // Need to allocate a new chunk - var newChunk = new Chuck(Chuck.CHUNK_SIZE, _entityCapacity); + var newChunk = new Chunk(Chunk.CHUNK_SIZE, _entityCapacity); rowIndex = 0; newChunk.Count++; @@ -234,9 +230,9 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable { var chunk = _chunks[chunkIndex]; var chunkBase = chunk.GetUnsafePtr(); - var pEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex); + var dst = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex); - MemoryUtility.MemCpy(&entity, pEntity, (nuint)sizeof(Entity)); + MemoryUtility.MemCpy(&entity, dst, (nuint)sizeof(Entity)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -253,7 +249,20 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref Chuck GetChunkReference(int index) + public void* GetComponentDataPtr(int chunkIndex, int rowIndex, Identifier componentID) + { + var offset = _componentIDToOffset[componentID]; + var chunk = _chunks[chunkIndex]; + + var chunkBase = chunk.GetUnsafePtr(); + var size = ComponentRegister.GetComponentInfo(componentID).size; + var dst = chunkBase + offset + (size * rowIndex); + + return dst; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref Chunk GetChunkReference(int index) { return ref _chunks[index]; } diff --git a/Ghost.Entities/EntityManager.cs b/Ghost.Entities/EntityManager.cs index ec0b37f..09efbbc 100644 --- a/Ghost.Entities/EntityManager.cs +++ b/Ghost.Entities/EntityManager.cs @@ -110,13 +110,13 @@ public unsafe class EntityManager : IDisposable ref Archetype newArch, int newChunk, int newRow) { // Iterate every component type in the OLD archetype - for (var i = 0; i < oldArch.Layouts.Count; i++) + for (var i = 0; i < oldArch._layouts.Count; i++) { - var layout = oldArch.Layouts[i]; + var layout = oldArch._layouts[i]; - var src = oldArch.Chunks[oldChunk].GetUnsafePtr() + layout.offset + (layout.size * oldRow); + var src = oldArch._chunks[oldChunk].GetUnsafePtr() + layout.offset + (layout.size * oldRow); var newOffset = newArch.GetOffset(layout.componentID); // O(1) Lookup - var dst = oldArch.Chunks[oldChunk].GetUnsafePtr() + newOffset + (layout.size * newRow); + var dst = oldArch._chunks[oldChunk].GetUnsafePtr() + newOffset + (layout.size * newRow); MemoryUtility.MemCpy(src, dst, (nuint)layout.size); } @@ -133,7 +133,7 @@ public unsafe class EntityManager : IDisposable // Build new archetype signature ref var oldArchetype = ref _world.GetArchetypeReference(location.archetypeID); - var oldSignature = oldArchetype.Signature; + var oldSignature = oldArchetype._signature; // TODO: Check edge cache first. var newArcID = oldArchetype.GetEdgeAdd(componentID); diff --git a/Ghost.Entities/EntityQuery.cs b/Ghost.Entities/EntityQuery.cs index 7def3aa..b284771 100644 --- a/Ghost.Entities/EntityQuery.cs +++ b/Ghost.Entities/EntityQuery.cs @@ -1,6 +1,6 @@ namespace Ghost.Entities; -public unsafe class EntityQuery +public unsafe class EntityQueryy where T1 : unmanaged, IComponent where T2 : unmanaged, IComponent { diff --git a/Ghost.Entities/Query.cs b/Ghost.Entities/Query.cs new file mode 100644 index 0000000..3f0f57f --- /dev/null +++ b/Ghost.Entities/Query.cs @@ -0,0 +1,263 @@ +using Ghost.Core; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; + +namespace Ghost.Entities; + +public struct EntityQueryMask : IDisposable +{ + public UnsafeBitSet all; + public UnsafeBitSet any; + public UnsafeBitSet absent; + + public bool Matches(UnsafeBitSet archetypeSignature) + { + // 1. Check All: Archetype must have ALL bits set + if (all.IsCreated && !archetypeSignature.All(all)) + { + return false; + } + + // 2. Check None: Archetype must have NONE of these bits set + if (absent.IsCreated && archetypeSignature.Any(absent)) + { + return false; + } + + // 3. Check Any: Archetype must have AT LEAST ONE of these bits (if Any is defined) + if (any.IsCreated && any.Count != 0 && !archetypeSignature.Any(any)) + { + return false; + } + + return true; + } + + public override int GetHashCode() + { + var hash = 17; + if (all.IsCreated) hash = hash * 23 + all.GetHashCode(); + if (absent.IsCreated) hash = hash * 23 + absent.GetHashCode(); + if (any.IsCreated) hash = hash * 23 + any.GetHashCode(); + + return hash; + } + + public void Dispose() + { + all.Dispose(); + any.Dispose(); + absent.Dispose(); + } +} + +public unsafe struct EntityQuery : IIdentifierType, IDisposable +{ + public readonly ref struct QueryItem + { + private readonly ref Archetype _archetype; + private readonly ref Chunk _chunk; + + public readonly int Count => _chunk.Count; + + internal QueryItem(ref Archetype archetype, int chunkIndex) + { + _archetype = ref archetype; + _chunk = ref archetype.GetChunkReference(chunkIndex); + } + + public readonly Span GetComponentData() + where T : unmanaged, IComponent + { + var offset = _archetype.GetOffset(ComponentTypeID.value); + if (offset < 0) + { + throw new InvalidOperationException($"Archetype does not contain component of type {typeof(T)}"); + } + + var ptr = (byte*)_chunk.GetUnsafePtr() + offset; + return new Span(ptr, _chunk.Count); + } + } + + public ref struct ChunkEnumerator + { + private ReadOnlyUnsafeCollection> _matchingArchetypes; + private World _world; + private int _archetypeIndex; + private int _chunkIndex; + + internal ChunkEnumerator(ReadOnlyUnsafeCollection> matchingArchetypes, World world) + { + _matchingArchetypes = matchingArchetypes; + _world = world; + _archetypeIndex = 0; + _chunkIndex = -1; + } + + public QueryItem Current + { + get + { + ref var archetype = ref _world.GetArchetypeReference(_matchingArchetypes[_archetypeIndex]); + return new QueryItem(ref archetype, _chunkIndex); + } + } + + public bool MoveNext() + { + _chunkIndex++; + + while (_archetypeIndex < _matchingArchetypes.Count) + { + ref var archetype = ref _world.GetArchetypeReference(_matchingArchetypes[_archetypeIndex]); + if (_chunkIndex < archetype.ChunkCount) + { + return true; + } + + _chunkIndex = 0; + _archetypeIndex++; + } + + return false; + } + + public void Reset() + { + _archetypeIndex = 0; + _chunkIndex = -1; + } + + public void Dispose() + { + } + } + + private Identifier _worldID; + private EntityQueryMask _mask; + private UnsafeList> _matchingArchetypes; + + internal EntityQuery(Identifier worldID, EntityQueryMask mask) + { + _worldID = worldID; + _mask = mask; + _matchingArchetypes = new UnsafeList>(8, Allocator.Persistent); + } + + // Called by World when a new archetype is created + internal void AddArchetypeIfMatch(Archetype archetype) + { + if (_mask.Matches(archetype._signature)) + { + _matchingArchetypes.Add(archetype.ID); + } + } + + public ChunkEnumerator GetEnumerator() + { + var world = World.GetWorld(_worldID).Value; + return new ChunkEnumerator(_matchingArchetypes.AsReadOnly(), world); + } + + public void Dispose() + { + _mask.Dispose(); + _matchingArchetypes.Dispose(); + } +} + +public ref struct QueryBuilder +{ + private Stack.Scope _scope; + + private UnsafeList> _all; + private UnsafeList> _any; + private UnsafeList> _absent; + + public QueryBuilder() + { + _scope = AllocationManager.CreateStackScope(); + + _all = new UnsafeList>(4, Allocator.Stack); + _any = new UnsafeList>(4, Allocator.Stack); + _absent = new UnsafeList>(4, Allocator.Stack); + } + + public QueryBuilder WithAll() + where T : unmanaged, IComponent + { + _all.Add(ComponentTypeID.value); + return this; + } + + public QueryBuilder WithAny() + where T : unmanaged, IComponent + { + _any.Add(ComponentTypeID.value); + return this; + } + + public QueryBuilder WithNone() + where T : unmanaged, IComponent + { + _absent.Add(ComponentTypeID.value); + return this; + } + + public Identifier Build(World world) + { + // 1. Calculate max component ID to size the BitSets + int maxID = 0; + FindMax(_all, ref maxID); + FindMax(_any, ref maxID); + FindMax(_absent, ref maxID); + + // 2. Create the Mask + using var mask = new EntityQueryMask + { + all = new UnsafeBitSet(maxID + 1, Allocator.Stack), + any = new UnsafeBitSet(maxID + 1, Allocator.Stack), + absent = new UnsafeBitSet(maxID + 1, Allocator.Stack) + }; + + // 3. Fill BitSets + foreach (var id in _all) mask.all.SetBit(id); + foreach (var id in _any) mask.any.SetBit(id); + foreach (var id in _absent) mask.absent.SetBit(id); + + // 4. Ask World for the Query (Cached) + var queryID = world.GetEntityQueryIDByMaskHash(mask.GetHashCode()); + if (queryID.IsNotValid) + { + queryID = world.CreateEntityQuery(mask); + ref var query = ref world.GetEntityQueryReference(queryID); + for (var i = 0; i < world.ArchetypeCount; i++) + { + ref var archetype = ref world.GetArchetypeReference(i); + query.AddArchetypeIfMatch(archetype); + } + } + + Dispose(); + + return queryID; + } + + private void FindMax(UnsafeList> list, ref int max) + { + foreach (var id in list) + { + if (id.value > max) max = id.value; + } + } + + private void Dispose() + { + _all.Dispose(); + _any.Dispose(); + _absent.Dispose(); + + _scope.Dispose(); + } +} diff --git a/Ghost.Entities/World.cs b/Ghost.Entities/World.cs index 6ec802d..822aaa5 100644 --- a/Ghost.Entities/World.cs +++ b/Ghost.Entities/World.cs @@ -32,6 +32,23 @@ public partial class World } } + public static void Destroy(Identifier id) + { + lock (s_worlds) + { + if (id.value < 0 || id.value >= s_worlds.Count) + { + return; + } + + var world = s_worlds[id.value]; + if (world is not null) + { + world.Dispose(); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Result GetWorld(Identifier id) { @@ -54,13 +71,19 @@ public partial class World : IIdentifierType, IDisposable, IEquatable { private readonly Identifier _id; - private UnsafeList _archetypes; - private UnsafeHashMap> _archetypeLookup; // Signature Hash to Archetype ID private EntityManager _entityManager; private EntityCommandBuffer _entityCommandBuffer; + private UnsafeList _archetypes; + private UnsafeList _entityQueries; + + private UnsafeHashMap> _archetypeLookup; // Signature Hash to Archetype ID + private UnsafeHashMap> _querieLookup; // Query Mask Hash to Query ID + private bool _disposed = false; + internal int ArchetypeCount => _archetypes.Count; + public Identifier ID => _id; public EntityManager EntityManager => _entityManager; public EntityCommandBuffer EntityCommandBuffer => _entityCommandBuffer; @@ -70,7 +93,10 @@ public partial class World : IIdentifierType, IDisposable, IEquatable _id = id; _archetypes = new UnsafeList(16, Allocator.Persistent); + _entityQueries = new UnsafeList(16, Allocator.Persistent); + _archetypeLookup = new UnsafeHashMap>(16, Allocator.Persistent); + _querieLookup = new UnsafeHashMap>(16, Allocator.Persistent); _entityManager = new EntityManager(this, entityCapacity); _entityCommandBuffer = new EntityCommandBuffer(_entityManager); @@ -90,12 +116,13 @@ public partial class World : IIdentifierType, IDisposable, IEquatable _archetypes.Add(new Archetype(arcID, _id, componentTypeIDs)); _archetypeLookup.Add(signatureHash, arcID); - return arcID; - } + for (int i = 0; i < _entityQueries.Count; i++) + { + ref var query = ref _entityQueries[i]; + query.AddArchetypeIfMatch(_archetypes[arcID.value]); + } - internal ref Archetype GetArchetypeReference(Identifier id) - { - return ref _archetypes[id.value]; + return arcID; } internal Identifier GetArchetypeIDBySignatureHash(int signatureHash) @@ -108,6 +135,35 @@ public partial class World : IIdentifierType, IDisposable, IEquatable return Identifier.Invalid; } + internal ref Archetype GetArchetypeReference(Identifier id) + { + return ref _archetypes[id.value]; + } + + internal Identifier CreateEntityQuery(EntityQueryMask mask) + { + var queryID = new Identifier(_entityQueries.Count); + _entityQueries.Add(new EntityQuery(_id, mask)); + _querieLookup.Add(mask.GetHashCode(), queryID); + + return queryID; + } + + internal Identifier GetEntityQueryIDByMaskHash(int maskHash) + { + if (_querieLookup.TryGetValue(maskHash, out var queryID)) + { + return queryID; + } + + return Identifier.Invalid; + } + + public ref EntityQuery GetEntityQueryReference(Identifier id) + { + return ref _entityQueries[id.value]; + } + public bool Equals(World? other) { return other is not null && _id == other._id; @@ -141,11 +197,15 @@ public partial class World : IIdentifierType, IDisposable, IEquatable } _entityManager.Dispose(); + _entityCommandBuffer.Dispose(); _archetypes.Dispose(); + _entityQueries.Dispose(); _archetypeLookup.Dispose(); + _querieLookup.Dispose(); s_freeWorldSlots.Enqueue(_id); + s_worlds[_id] = null; _disposed = true;