Updated entity query

This commit is contained in:
2025-12-04 20:37:52 +09:00
parent 93bc8e55a3
commit f9db047a5f
6 changed files with 378 additions and 36 deletions

View File

@@ -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<byte>(size, Allocator.Persistent);
_capacity = capacity;
@@ -59,11 +59,11 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
private readonly Identifier<Archetype> _id;
private readonly Identifier<World> _worldID;
private UnsafeBitSet _signature;
private UnsafeList<Chuck> _chunks;
private UnsafeArray<ComponentMemoryLayout> _layouts;
private UnsafeArray<int> _componentIDToOffset;
internal UnsafeBitSet _signature;
internal UnsafeList<Chunk> _chunks;
internal UnsafeArray<ComponentMemoryLayout> _layouts;
private UnsafeArray<int> _componentIDToOffset;
// TODO: Is hash map better?
private UnsafeList<Edge> _edgesAdd;
private UnsafeList<Edge> _edgesRemove;
@@ -75,10 +75,6 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
public Identifier<Archetype> ID => _id;
public UnsafeBitSet Signature => _signature;
public UnsafeList<Chuck> Chunks => _chunks;
public UnsafeArray<ComponentMemoryLayout> 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<Chuck>(4, Allocator.Persistent);
_chunks = new UnsafeList<Chunk>(4, Allocator.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
_edgesRemove = new UnsafeList<Edge>(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<Chuck>(4, Allocator.Persistent);
_chunks = new UnsafeList<Chunk>(4, Allocator.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
_edgesRemove = new UnsafeList<Edge>(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<ComponentMemoryLayout>(components.Length, Allocator.Persistent);
_componentIDToOffset = new UnsafeArray<int>(_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<IComponent> 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];
}

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
namespace Ghost.Entities;
public unsafe class EntityQuery<T1, T2>
public unsafe class EntityQueryy<T1, T2>
where T1 : unmanaged, IComponent
where T2 : unmanaged, IComponent
{

263
Ghost.Entities/Query.cs Normal file
View File

@@ -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<T> GetComponentData<T>()
where T : unmanaged, IComponent
{
var offset = _archetype.GetOffset(ComponentTypeID<T>.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<T>(ptr, _chunk.Count);
}
}
public ref struct ChunkEnumerator
{
private ReadOnlyUnsafeCollection<Identifier<Archetype>> _matchingArchetypes;
private World _world;
private int _archetypeIndex;
private int _chunkIndex;
internal ChunkEnumerator(ReadOnlyUnsafeCollection<Identifier<Archetype>> 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<World> _worldID;
private EntityQueryMask _mask;
private UnsafeList<Identifier<Archetype>> _matchingArchetypes;
internal EntityQuery(Identifier<World> worldID, EntityQueryMask mask)
{
_worldID = worldID;
_mask = mask;
_matchingArchetypes = new UnsafeList<Identifier<Archetype>>(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<Identifier<IComponent>> _all;
private UnsafeList<Identifier<IComponent>> _any;
private UnsafeList<Identifier<IComponent>> _absent;
public QueryBuilder()
{
_scope = AllocationManager.CreateStackScope();
_all = new UnsafeList<Identifier<IComponent>>(4, Allocator.Stack);
_any = new UnsafeList<Identifier<IComponent>>(4, Allocator.Stack);
_absent = new UnsafeList<Identifier<IComponent>>(4, Allocator.Stack);
}
public QueryBuilder WithAll<T>()
where T : unmanaged, IComponent
{
_all.Add(ComponentTypeID<T>.value);
return this;
}
public QueryBuilder WithAny<T>()
where T : unmanaged, IComponent
{
_any.Add(ComponentTypeID<T>.value);
return this;
}
public QueryBuilder WithNone<T>()
where T : unmanaged, IComponent
{
_absent.Add(ComponentTypeID<T>.value);
return this;
}
public Identifier<EntityQuery> 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<Identifier<IComponent>> 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();
}
}

View File

@@ -32,6 +32,23 @@ public partial class World
}
}
public static void Destroy(Identifier<World> 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<World, ResultStatus> GetWorld(Identifier<World> id)
{
@@ -54,13 +71,19 @@ public partial class World : IIdentifierType, IDisposable, IEquatable<World>
{
private readonly Identifier<World> _id;
private UnsafeList<Archetype> _archetypes;
private UnsafeHashMap<int, Identifier<Archetype>> _archetypeLookup; // Signature Hash to Archetype ID
private EntityManager _entityManager;
private EntityCommandBuffer _entityCommandBuffer;
private UnsafeList<Archetype> _archetypes;
private UnsafeList<EntityQuery> _entityQueries;
private UnsafeHashMap<int, Identifier<Archetype>> _archetypeLookup; // Signature Hash to Archetype ID
private UnsafeHashMap<int, Identifier<EntityQuery>> _querieLookup; // Query Mask Hash to Query ID
private bool _disposed = false;
internal int ArchetypeCount => _archetypes.Count;
public Identifier<World> ID => _id;
public EntityManager EntityManager => _entityManager;
public EntityCommandBuffer EntityCommandBuffer => _entityCommandBuffer;
@@ -70,7 +93,10 @@ public partial class World : IIdentifierType, IDisposable, IEquatable<World>
_id = id;
_archetypes = new UnsafeList<Archetype>(16, Allocator.Persistent);
_entityQueries = new UnsafeList<EntityQuery>(16, Allocator.Persistent);
_archetypeLookup = new UnsafeHashMap<int, Identifier<Archetype>>(16, Allocator.Persistent);
_querieLookup = new UnsafeHashMap<int, Identifier<EntityQuery>>(16, Allocator.Persistent);
_entityManager = new EntityManager(this, entityCapacity);
_entityCommandBuffer = new EntityCommandBuffer(_entityManager);
@@ -90,12 +116,13 @@ public partial class World : IIdentifierType, IDisposable, IEquatable<World>
_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<Archetype> id)
{
return ref _archetypes[id.value];
return arcID;
}
internal Identifier<Archetype> GetArchetypeIDBySignatureHash(int signatureHash)
@@ -108,6 +135,35 @@ public partial class World : IIdentifierType, IDisposable, IEquatable<World>
return Identifier<Archetype>.Invalid;
}
internal ref Archetype GetArchetypeReference(Identifier<Archetype> id)
{
return ref _archetypes[id.value];
}
internal Identifier<EntityQuery> CreateEntityQuery(EntityQueryMask mask)
{
var queryID = new Identifier<EntityQuery>(_entityQueries.Count);
_entityQueries.Add(new EntityQuery(_id, mask));
_querieLookup.Add(mask.GetHashCode(), queryID);
return queryID;
}
internal Identifier<EntityQuery> GetEntityQueryIDByMaskHash(int maskHash)
{
if (_querieLookup.TryGetValue(maskHash, out var queryID))
{
return queryID;
}
return Identifier<EntityQuery>.Invalid;
}
public ref EntityQuery GetEntityQueryReference(Identifier<EntityQuery> 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<World>
}
_entityManager.Dispose();
_entityCommandBuffer.Dispose();
_archetypes.Dispose();
_entityQueries.Dispose();
_archetypeLookup.Dispose();
_querieLookup.Dispose();
s_freeWorldSlots.Enqueue(_id);
s_worlds[_id] = null;
_disposed = true;