From 93bc8e55a3d446953f9e5d7b1ba90730ff15c278 Mon Sep 17 00:00:00 2001 From: Misaki Date: Thu, 4 Dec 2025 16:55:26 +0900 Subject: [PATCH] Changed project name --- Ghost.ArcEntities/Ghost.ArcEntities.csproj | 14 +- .../Controls/Internal/ComponentDataView.cs | 2 +- .../Inspector/ComponentObject.cs | 6 +- Ghost.Editor.Core/SceneGraph/EntityNode.cs | 2 +- .../SceneGraph/SceneGraphHelpers.cs | 2 +- Ghost.Editor.Core/SceneGraph/WorldNode.cs | 2 +- .../Serializer/WorldNodeSerializer.cs | 4 +- Ghost.Engine/Components/Hierarchy.cs | 4 +- Ghost.Engine/Components/LocalToWorld.cs | 2 +- Ghost.Engine/Ghost.Engine.csproj | 2 +- Ghost.Engine/Services/PlayerLoopService.cs | 2 +- Ghost.Entities.Test/ArcEntityTest.cs | 5 +- Ghost.Entities.Test/EntityTest.cs | 12 +- .../Ghost.Entities.Test.csproj | 2 +- Ghost.Entities.Test/Program.cs | 3 +- Ghost.Entities/Archetype.cs | 417 +++++++++++++++ Ghost.Entities/AssemblyInfo.cs | 11 +- Ghost.Entities/Component.cs | 96 ++++ Ghost.Entities/Components/IComponentData.cs | 5 - Ghost.Entities/Entity.cs | 34 +- Ghost.Entities/EntityCommandBuffer.cs | 110 ++++ Ghost.Entities/EntityManager.cs | 484 +++++++----------- Ghost.Entities/EntityQuery.cs | 60 +++ Ghost.Entities/Ghost.Entities.csproj | 101 +--- Ghost.Entities/Template/ForEach.cs | 12 - Ghost.Entities/World.cs | 137 +++-- Ghost.Graphics/Ghost.Graphics.csproj | 6 - Ghost.SparseEntities/AssemblyInfo.cs | 12 + .../Components/ComponentStorage.cs | 2 +- .../Components/IComponentData.cs | 5 + .../Components/ScriptComponent.cs | 2 +- Ghost.SparseEntities/Entity.cs | 82 +++ Ghost.SparseEntities/EntityManager.cs | 360 +++++++++++++ .../Ghost.SparseEntities.csproj | 113 ++++ .../Query/QueryBuilder.cs | 2 +- .../Query/QueryFilter.cs | 2 +- .../Query/QueryTypeParameter.cs | 4 +- .../Systems/ISystem.cs | 2 +- .../Systems/SystemDependencyBuilder.cs | 2 +- .../Systems/SystemState.cs | 2 +- .../Systems/SystemStorage.cs | 2 +- Ghost.SparseEntities/Template/ForEach.cs | 12 + .../Template/ForEach.tt | 0 .../Template/Helpers.ttinclude | 0 .../Template/QueryEnumerable.cs | 6 +- .../Template/QueryEnumerable.tt | 0 .../Template/QueryItem.cs | 78 +-- .../Template/QueryItem.tt | 0 .../Template/QueryRefComponent.cs | 18 +- .../Template/QueryRefComponent.tt | 0 .../Template/World.Query.cs | 22 +- .../Template/World.Query.tt | 0 Ghost.SparseEntities/World.cs | 164 ++++++ GhostEngine.slnx | 3 +- 54 files changed, 1819 insertions(+), 613 deletions(-) create mode 100644 Ghost.Entities/Archetype.cs create mode 100644 Ghost.Entities/Component.cs delete mode 100644 Ghost.Entities/Components/IComponentData.cs create mode 100644 Ghost.Entities/EntityCommandBuffer.cs create mode 100644 Ghost.Entities/EntityQuery.cs delete mode 100644 Ghost.Entities/Template/ForEach.cs create mode 100644 Ghost.SparseEntities/AssemblyInfo.cs rename {Ghost.Entities => Ghost.SparseEntities}/Components/ComponentStorage.cs (99%) create mode 100644 Ghost.SparseEntities/Components/IComponentData.cs rename {Ghost.Entities => Ghost.SparseEntities}/Components/ScriptComponent.cs (98%) create mode 100644 Ghost.SparseEntities/Entity.cs create mode 100644 Ghost.SparseEntities/EntityManager.cs create mode 100644 Ghost.SparseEntities/Ghost.SparseEntities.csproj rename {Ghost.Entities => Ghost.SparseEntities}/Query/QueryBuilder.cs (94%) rename {Ghost.Entities => Ghost.SparseEntities}/Query/QueryFilter.cs (98%) rename {Ghost.Entities => Ghost.SparseEntities}/Query/QueryTypeParameter.cs (94%) rename {Ghost.Entities => Ghost.SparseEntities}/Systems/ISystem.cs (93%) rename {Ghost.Entities => Ghost.SparseEntities}/Systems/SystemDependencyBuilder.cs (99%) rename {Ghost.Entities => Ghost.SparseEntities}/Systems/SystemState.cs (68%) rename {Ghost.Entities => Ghost.SparseEntities}/Systems/SystemStorage.cs (98%) create mode 100644 Ghost.SparseEntities/Template/ForEach.cs rename {Ghost.Entities => Ghost.SparseEntities}/Template/ForEach.tt (100%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/Helpers.ttinclude (100%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/QueryEnumerable.cs (99%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/QueryEnumerable.tt (100%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/QueryItem.cs (85%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/QueryItem.tt (100%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/QueryRefComponent.cs (69%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/QueryRefComponent.tt (100%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/World.Query.cs (96%) rename {Ghost.Entities => Ghost.SparseEntities}/Template/World.Query.tt (100%) create mode 100644 Ghost.SparseEntities/World.cs diff --git a/Ghost.ArcEntities/Ghost.ArcEntities.csproj b/Ghost.ArcEntities/Ghost.ArcEntities.csproj index e73b545..7fa3d2d 100644 --- a/Ghost.ArcEntities/Ghost.ArcEntities.csproj +++ b/Ghost.ArcEntities/Ghost.ArcEntities.csproj @@ -4,11 +4,21 @@ net10.0 enable enable - true + True + + + + True + True + + + + True + True - + diff --git a/Ghost.Editor.Core/Controls/Internal/ComponentDataView.cs b/Ghost.Editor.Core/Controls/Internal/ComponentDataView.cs index ec69645..bbfe676 100644 --- a/Ghost.Editor.Core/Controls/Internal/ComponentDataView.cs +++ b/Ghost.Editor.Core/Controls/Internal/ComponentDataView.cs @@ -2,7 +2,7 @@ using Ghost.Core; using Ghost.Editor.Core.Inspector; using Ghost.Editor.Core.Resources; using Ghost.Editor.Core.Utilities; -using Ghost.Entities; +using Ghost.SparseEntities; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System.Reflection; diff --git a/Ghost.Editor.Core/Inspector/ComponentObject.cs b/Ghost.Editor.Core/Inspector/ComponentObject.cs index 7cf7214..6c9a53c 100644 --- a/Ghost.Editor.Core/Inspector/ComponentObject.cs +++ b/Ghost.Editor.Core/Inspector/ComponentObject.cs @@ -1,6 +1,6 @@ -using Ghost.Entities; -using Ghost.Entities.Components; -using Ghost.Entities.Query; +using Ghost.SparseEntities; +using Ghost.SparseEntities.Components; +using Ghost.SparseEntities.Query; namespace Ghost.Editor.Core.Inspector; diff --git a/Ghost.Editor.Core/SceneGraph/EntityNode.cs b/Ghost.Editor.Core/SceneGraph/EntityNode.cs index 1596d1c..5ebc79a 100644 --- a/Ghost.Editor.Core/SceneGraph/EntityNode.cs +++ b/Ghost.Editor.Core/SceneGraph/EntityNode.cs @@ -2,7 +2,7 @@ using Ghost.Editor.Core.Controls.Internal; using Ghost.Editor.Core.Inspector; using Ghost.Editor.Core.Resources; using Ghost.Engine.Editor; -using Ghost.Entities; +using Ghost.SparseEntities; using Microsoft.UI.Text; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; diff --git a/Ghost.Editor.Core/SceneGraph/SceneGraphHelpers.cs b/Ghost.Editor.Core/SceneGraph/SceneGraphHelpers.cs index c3996ea..f7af922 100644 --- a/Ghost.Editor.Core/SceneGraph/SceneGraphHelpers.cs +++ b/Ghost.Editor.Core/SceneGraph/SceneGraphHelpers.cs @@ -1,5 +1,5 @@ using Ghost.Engine.Components; -using Ghost.Entities; +using Ghost.SparseEntities; namespace Ghost.Editor.Core.SceneGraph; diff --git a/Ghost.Editor.Core/SceneGraph/WorldNode.cs b/Ghost.Editor.Core/SceneGraph/WorldNode.cs index b50d699..415c2d2 100644 --- a/Ghost.Editor.Core/SceneGraph/WorldNode.cs +++ b/Ghost.Editor.Core/SceneGraph/WorldNode.cs @@ -3,7 +3,7 @@ using Ghost.Editor.Core.Inspector; using Ghost.Editor.Core.Resources; using Ghost.Editor.Core.Serializer; using Ghost.Engine.Components; -using Ghost.Entities; +using Ghost.SparseEntities; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System.Text.Json.Serialization; diff --git a/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs b/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs index 22e5467..3c27e77 100644 --- a/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs +++ b/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs @@ -1,7 +1,7 @@ using Ghost.Editor.Core.SceneGraph; using Ghost.Engine.Utilities; -using Ghost.Entities; -using Ghost.Entities.Components; +using Ghost.SparseEntities; +using Ghost.SparseEntities.Components; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Ghost.Engine/Components/Hierarchy.cs b/Ghost.Engine/Components/Hierarchy.cs index ac95da2..5556ac4 100644 --- a/Ghost.Engine/Components/Hierarchy.cs +++ b/Ghost.Engine/Components/Hierarchy.cs @@ -1,6 +1,6 @@ using Ghost.Engine.Editor; -using Ghost.Entities; -using Ghost.Entities.Components; +using Ghost.SparseEntities; +using Ghost.SparseEntities.Components; using System.Runtime.CompilerServices; namespace Ghost.Engine.Components; diff --git a/Ghost.Engine/Components/LocalToWorld.cs b/Ghost.Engine/Components/LocalToWorld.cs index d89038a..abd7e65 100644 --- a/Ghost.Engine/Components/LocalToWorld.cs +++ b/Ghost.Engine/Components/LocalToWorld.cs @@ -1,5 +1,5 @@ using Ghost.Engine.Utilities; -using Ghost.Entities.Components; +using Ghost.SparseEntities.Components; using System.Numerics; using System.Runtime.CompilerServices; diff --git a/Ghost.Engine/Ghost.Engine.csproj b/Ghost.Engine/Ghost.Engine.csproj index c556cea..6610be3 100644 --- a/Ghost.Engine/Ghost.Engine.csproj +++ b/Ghost.Engine/Ghost.Engine.csproj @@ -16,7 +16,7 @@ - + diff --git a/Ghost.Engine/Services/PlayerLoopService.cs b/Ghost.Engine/Services/PlayerLoopService.cs index d4d999c..61e11a5 100644 --- a/Ghost.Engine/Services/PlayerLoopService.cs +++ b/Ghost.Engine/Services/PlayerLoopService.cs @@ -1,4 +1,4 @@ -using Ghost.Entities; +using Ghost.SparseEntities; namespace Ghost.Engine.Services; diff --git a/Ghost.Entities.Test/ArcEntityTest.cs b/Ghost.Entities.Test/ArcEntityTest.cs index 5ba065f..ee574f7 100644 --- a/Ghost.Entities.Test/ArcEntityTest.cs +++ b/Ghost.Entities.Test/ArcEntityTest.cs @@ -1,16 +1,15 @@ using Ghost.Test.Core; -using Ghost.ArcEntities; using Misaki.HighPerformance.Mathematics; namespace Ghost.Entities.Test; public partial class ArcEntityTest : ITest { - private Ghost.ArcEntities.World _world = null!; + private World _world = null!; public void Setup() { - _world = Ghost.ArcEntities.World.Create(); + _world = World.Create(); } public void Run() diff --git a/Ghost.Entities.Test/EntityTest.cs b/Ghost.Entities.Test/EntityTest.cs index 2d43afa..039e2d8 100644 --- a/Ghost.Entities.Test/EntityTest.cs +++ b/Ghost.Entities.Test/EntityTest.cs @@ -1,6 +1,7 @@ -using Ghost.Entities.Components; -using Ghost.Entities.Query; -using Ghost.Entities.Systems; +#if false +using Ghost.SparseEntities.Components; +using Ghost.SparseEntities.Query; +using Ghost.SparseEntities.Systems; using Ghost.Test.Core; using System.Numerics; @@ -128,7 +129,7 @@ public class UserScript : ScriptComponent EntityManager.GetComponent(Owner).ValueRW.position += new Vector3(10, 10, 10); } - override public void OnDisable() + public override void OnDisable() { Console.WriteLine("UserScript disabled for entity: " + Owner); } @@ -188,4 +189,5 @@ public class EventManager : ScriptComponent { Console.WriteLine("EventManager destroyed for entity: " + Owner); } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/Ghost.Entities.Test/Ghost.Entities.Test.csproj b/Ghost.Entities.Test/Ghost.Entities.Test.csproj index 9ab4993..f1e1ac5 100644 --- a/Ghost.Entities.Test/Ghost.Entities.Test.csproj +++ b/Ghost.Entities.Test/Ghost.Entities.Test.csproj @@ -9,7 +9,7 @@ - + diff --git a/Ghost.Entities.Test/Program.cs b/Ghost.Entities.Test/Program.cs index b60f772..67483f2 100644 --- a/Ghost.Entities.Test/Program.cs +++ b/Ghost.Entities.Test/Program.cs @@ -1,3 +1,4 @@ +using Ghost.Entities.Test; using Ghost.Test.Core; -TestRunner.Run(); +TestRunner.Run(); \ No newline at end of file diff --git a/Ghost.Entities/Archetype.cs b/Ghost.Entities/Archetype.cs new file mode 100644 index 0000000..5856782 --- /dev/null +++ b/Ghost.Entities/Archetype.cs @@ -0,0 +1,417 @@ +using Ghost.Core; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Utilities; +using System.Runtime.CompilerServices; + +namespace Ghost.Entities; + +internal unsafe struct Chuck : IDisposable +{ + public const int CHUNK_SIZE = 16384; // 16 KB + + private UnsafeArray _data; + private int _count; + private int _capacity; + + public int Count + { + get => _count; + set => _count = value; + } + + public int Capacity => _capacity; + + public Chuck(int size, int capacity) + { + _data = new UnsafeArray(size, Allocator.Persistent); + _capacity = capacity; + _count = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte* GetUnsafePtr() + { + return (byte*)_data.GetUnsafePtr(); + } + + public void Dispose() + { + _data.Dispose(); + } +} + +internal struct Edge +{ + public Identifier componentID; + public int targetArchetype; // can't use Identifier because cycle causer +} + +internal struct ComponentMemoryLayout +{ + public int offset; + public int size; + public Identifier componentID; +} + +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; + + // TODO: Is hash map better? + private UnsafeList _edgesAdd; + private UnsafeList _edgesRemove; + + private int _hash; + private int _entityCapacity; + private int _maxComponentID; + private int _entityIdsOffset; + + 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; + + public Archetype(Identifier id, Identifier worldID, ReadOnlySpan> componentIds) + { + _id = id; + _worldID = worldID; + + if (componentIds.IsEmpty) + { + _signature = new UnsafeBitSet(1, Allocator.Persistent, AllocationOption.Clear); + _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); + + return; + } + + var highestComponentID = 0; + for (var i = 0; i < componentIds.Length; i++) + { + if (componentIds[i] > highestComponentID) + { + highestComponentID = componentIds[i]; + } + } + + _signature = new UnsafeBitSet(highestComponentID + 1, Allocator.Persistent, AllocationOption.Clear); + _chunks = new UnsafeList(4, Allocator.Persistent); + + _edgesAdd = new UnsafeList(4, Allocator.Persistent); + _edgesRemove = new UnsafeList(4, Allocator.Persistent); + + _hash = _signature.GetHashCode(); + + var pComponents = stackalloc ComponentInfo[componentIds.Length]; + for (var i = 0; i < componentIds.Length; i++) + { + _signature.SetBit(componentIds[i]); + pComponents[i] = ComponentRegister.GetComponentInfo(componentIds[i]); + } + + CalculateLayout(new Span(pComponents, componentIds.Length)); + } + + private void CalculateLayout(Span components) + { + var entitySize = sizeof(Entity); + var entityAlign = (int)MemoryUtility.AlignOf(); + + // Calculate total size per entity to get an initial capacity estimate + var bytesPerEntity = entitySize; + var maxComponentID = 0; + for (var i = 0; i < components.Length; i++) + { + var comp = components[i]; + bytesPerEntity += comp.size; + if (comp.id > maxComponentID) + { + maxComponentID = comp.id; + } + } + + _maxComponentID = maxComponentID; + _entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity; + _layouts = new UnsafeArray(components.Length, Allocator.Persistent); + _componentIDToOffset = new UnsafeArray(_maxComponentID + 1, Allocator.Persistent); + + _componentIDToOffset.AsSpan().Fill(-1); + + components.Sort((a, b) => b.alignment.CompareTo(a.alignment)); + var tempOffsets = stackalloc int[components.Length]; + + while (_entityCapacity > 0) + { + var currentOffset = 0; + var fits = true; + + currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1); + + _entityIdsOffset = currentOffset; + currentOffset += _entityCapacity * entitySize; + + for (var i = 0; i < components.Length; i++) + { + var size = components[i].size; + var align = components[i].alignment; + + currentOffset = (currentOffset + align - 1) & ~(align - 1); + tempOffsets[i] = currentOffset; + currentOffset += _entityCapacity * size; + + if (currentOffset > Chuck.CHUNK_SIZE) + { + fits = false; + break; + } + } + + if (fits) + { + for (var i = 0; i < components.Length; i++) + { + _layouts[i] = new ComponentMemoryLayout + { + offset = tempOffsets[i], + size = components[i].size, + componentID = components[i].id + }; + + _componentIDToOffset[components[i].id] = tempOffsets[i]; + } + + return; + } + + _entityCapacity--; + } + } + + public void AllocateEntity(out int chunkIndex, out int rowIndex) + { + for (var i = 0; i < _chunks.Count; i++) + { + var chunk = _chunks[i]; + if (chunk.Count < _entityCapacity) + { + rowIndex = chunk.Count; + chunk.Count++; + chunkIndex = i; + + return; + } + } + + // Need to allocate a new chunk + var newChunk = new Chuck(Chuck.CHUNK_SIZE, _entityCapacity); + + rowIndex = 0; + newChunk.Count++; + chunkIndex = _chunks.Count; + + _chunks.Add(newChunk); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetEntity(int chunkIndex, int rowIndex, Entity entity) + { + var chunk = _chunks[chunkIndex]; + var chunkBase = chunk.GetUnsafePtr(); + var pEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex); + + MemoryUtility.MemCpy(&entity, pEntity, (nuint)sizeof(Entity)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetComponentData(int chunkIndex, int rowIndex, Identifier componentID, void* pComponent) + { + var offset = _componentIDToOffset[componentID]; + var chunk = _chunks[chunkIndex]; + + var chunkBase = chunk.GetUnsafePtr(); + var size = ComponentRegister.GetComponentInfo(componentID).size; + var dst = chunkBase + offset + (size * rowIndex); + + MemoryUtility.MemCpy(pComponent, dst, (nuint)size); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref Chuck GetChunkReference(int index) + { + return ref _chunks[index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int componentId) + { + if (componentId >= _componentIDToOffset.Count) + { + return -1; + } + + return _componentIDToOffset[componentId]; + } + + public ResultStatus RemoveEntity(int chunkIndex, int rowIndex) + { + if (chunkIndex < 0 || chunkIndex >= _chunks.Count) + { + return ResultStatus.InvalidArgument; + } + + ref var chunk = ref _chunks[chunkIndex]; + var lastIndex = chunk.Count - 1; + + // If we are NOT removing the very last entity, we must swap. + if (rowIndex != lastIndex) + { + var chunkBase = chunk.GetUnsafePtr(); + var pLastEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * lastIndex); + var pRowEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex); + + var wroldResult = World.GetWorld(_worldID); + if (wroldResult.Status != ResultStatus.Success) + { + return wroldResult.Status; + } + + var result = wroldResult.Value.EntityManager.UpdateEntityLocation(*(Entity*)pLastEntity, _id, chunkIndex, rowIndex); + if (result != ResultStatus.Success) + { + return result; + } + + // Only operate the swap back after the update is succeed. + MemoryUtility.MemCpy(pLastEntity, pRowEntity, (nuint)sizeof(Entity)); + + for (var i = 0; i <= _layouts.Count; i++) + { + var layout = _layouts[i]; + + var pRow = chunk.GetUnsafePtr() + layout.offset + (layout.size * rowIndex); + var pLast = chunk.GetUnsafePtr() + layout.offset + (layout.size * lastIndex); + + MemoryUtility.MemCpy(pLast, pRow, (nuint)layout.size); + } + } + + chunk.Count--; + return ResultStatus.Success; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasComponent(Identifier componentID) + { + return _signature.IsSet(componentID); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddEdgeAdd(Identifier componentID, Identifier targetArchetype) + { + _edgesAdd.Add(new Edge + { + componentID = componentID, + targetArchetype = targetArchetype + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Identifier GetEdgeAdd(Identifier componentID) + { + for (var i = 0; i < _edgesAdd.Count; i++) + { + var edge = _edgesAdd[i]; + if (edge.componentID == componentID) + { + return edge.targetArchetype; + } + } + + return Identifier.Invalid; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddEdgeRemove(Identifier componentID, Identifier targetArchetype) + { + _edgesRemove.Add(new Edge + { + componentID = componentID, + targetArchetype = targetArchetype + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Identifier GetEdgeRemove(Identifier componentID) + { + for (var i = 0; i < _edgesRemove.Count; i++) + { + var edge = _edgesRemove[i]; + if (edge.componentID == componentID) + { + return edge.targetArchetype; + } + } + + return Identifier.Invalid; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetComponentArray(int chunkIndex) + where T : unmanaged, IComponent + { + var id = ComponentTypeID.value; + if (id >= _componentIDToOffset.Count) + { + return default; + } + + var offset = _componentIDToOffset[id]; + if (offset == -1) + { + return default; + } + + var chunk = _chunks[chunkIndex]; + return new Span((T*)((byte*)chunk.GetUnsafePtr() + offset), chunk.Count); + } + + public override int GetHashCode() + { + return _hash; + } + + public void Dispose() + { + if (_chunks.IsCreated) + { + foreach (ref var chunk in _chunks) + { + chunk.Dispose(); + } + } + + _signature.Dispose(); + _chunks.Dispose(); + _componentIDToOffset.Dispose(); + _layouts.Dispose(); + _edgesAdd.Dispose(); + _edgesRemove.Dispose(); + } +} diff --git a/Ghost.Entities/AssemblyInfo.cs b/Ghost.Entities/AssemblyInfo.cs index 6c605e9..6d2c349 100644 --- a/Ghost.Entities/AssemblyInfo.cs +++ b/Ghost.Entities/AssemblyInfo.cs @@ -1,12 +1,3 @@ global using EntityID = System.Int32; -global using GenerationID = System.UInt16; -global using WorldID = System.UInt16; +global using GenerationID = System.Int32; -using Ghost.Core.Attributes; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Ghost.Engine")] -[assembly: InternalsVisibleTo("Ghost.Editor.Core")] -[assembly: InternalsVisibleTo("Ghost.Entities.Test")] - -[assembly: EngineAssembly] \ No newline at end of file diff --git a/Ghost.Entities/Component.cs b/Ghost.Entities/Component.cs new file mode 100644 index 0000000..7ff1dae --- /dev/null +++ b/Ghost.Entities/Component.cs @@ -0,0 +1,96 @@ +using Ghost.Core; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Utilities; + +namespace Ghost.Entities; + +public interface IComponent : IIdentifierType +{ +} + +public struct ComponentInfo +{ + // public FixedText64 stableName; // Do we actually need this? + public int size; + public int alignment; + public Identifier id; +} + +public static unsafe class ComponentTypeID + where T : unmanaged, IComponent +{ + public static readonly Identifier value = ComponentRegister.GetOrRegisterComponent(); +} + +internal static class ComponentRegister +{ + private static int s_nextComponentTypeID = 0; + private static Dictionary> s_typeHandleToID = new(); + + private static List s_registeredComponents = new(); + private static Dictionary> s_nameToRuntimeID = new(); + + public unsafe static Identifier GetOrRegisterComponent() + where T : unmanaged, IComponent + { + var typeHandle = typeof(T).TypeHandle.Value; + + lock (s_registeredComponents) + { + if (s_typeHandleToID.TryGetValue(typeHandle, out var existingID)) + { + return existingID; + } + + var newID = new Identifier(s_nextComponentTypeID); + s_nextComponentTypeID++; + + var stableName = typeof(T).FullName ?? typeof(T).Name; + + var info = new ComponentInfo + { + // stableName = new FixedText64(stableName), + size = sizeof(T), + alignment = (int)MemoryUtility.AlignOf(), + id = newID, + }; + + while (s_registeredComponents.Count <= newID.value) s_registeredComponents.Add(default); + s_registeredComponents[newID.value] = info; + + s_typeHandleToID[typeHandle] = newID; + s_nameToRuntimeID[stableName] = newID; + + return newID; + } + } + + public static ComponentInfo GetComponentInfo(Identifier typeId) + { + return s_registeredComponents[typeId]; + } + + public static int GetHashCode(ReadOnlySpan> componentTypeIDs) + { + var largestID = 0; + foreach (var id in componentTypeIDs) + { + if (id.value > largestID) + { + largestID = id.value; + } + } + + var length = UnsafeBitSet.RequiredLength(largestID + 1); + var bits = (Span)stackalloc uint[length]; + bits.Clear(); + + var bitSet = new SpanBitSet(bits); + foreach (var id in componentTypeIDs) + { + bitSet.SetBit(id.value); + } + + return bitSet.GetHashCode(); + } +} diff --git a/Ghost.Entities/Components/IComponentData.cs b/Ghost.Entities/Components/IComponentData.cs deleted file mode 100644 index 6a6eb6e..0000000 --- a/Ghost.Entities/Components/IComponentData.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Ghost.Entities.Components; - -public interface IComponentData -{ -} \ No newline at end of file diff --git a/Ghost.Entities/Entity.cs b/Ghost.Entities/Entity.cs index 8767dff..ae99fd2 100644 --- a/Ghost.Entities/Entity.cs +++ b/Ghost.Entities/Entity.cs @@ -1,30 +1,29 @@ using System.Runtime.CompilerServices; -using System.Text.Json.Serialization; +using System.Runtime.InteropServices; namespace Ghost.Entities; -[SkipLocalsInit] -public struct Entity : IEquatable, IComparable +[StructLayout(LayoutKind.Sequential, Size = 8)] +public readonly struct Entity : IEquatable, IComparable { public const EntityID INVALID_ID = -1; - [JsonInclude] - private EntityID _id; - private GenerationID _generation; + private readonly EntityID _id; + private readonly GenerationID _generation; - public readonly EntityID ID + public EntityID ID { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _id; } - public readonly GenerationID Generation + public GenerationID Generation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _generation; } - public readonly bool IsValid + public bool IsValid { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ID != INVALID_ID; @@ -42,27 +41,24 @@ public struct Entity : IEquatable, IComparable _generation = generation; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void IncrementGeneration() => _generation++; - - public readonly bool Equals(Entity other) + public bool Equals(Entity other) { return _id == other._id && _generation == other._generation; } - public readonly int CompareTo(Entity other) + public int CompareTo(Entity other) { return _id.CompareTo(other._id); } - public override readonly bool Equals(object? obj) + public override bool Equals(object? obj) { return obj is Entity other && Equals(other); } - public override readonly int GetHashCode() + public override int GetHashCode() { - return _id.GetHashCode(); + return _id ^ _generation << 16; } public static bool operator ==(Entity left, Entity right) @@ -75,8 +71,8 @@ public struct Entity : IEquatable, IComparable return !(left == right); } - public override readonly string ToString() + public override string ToString() { return $"Entity {{ Index: {ID}, Generation: {Generation} }}"; } -} \ No newline at end of file +} diff --git a/Ghost.Entities/EntityCommandBuffer.cs b/Ghost.Entities/EntityCommandBuffer.cs new file mode 100644 index 0000000..7a4d6ad --- /dev/null +++ b/Ghost.Entities/EntityCommandBuffer.cs @@ -0,0 +1,110 @@ +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Utilities; + +namespace Ghost.Entities; + +public unsafe class EntityCommandBuffer : IDisposable +{ + private enum CommandType + { + CreateEntity, + DestroyEntity, + AddComponent, + RemoveComponent, + SetComponent, + } + + private struct Command + { + public UnsafeArray data; + public CommandType type; + public Entity entity; + public int componentTypeID; + } + + private readonly EntityManager _entityManager; + private UnsafeList _commands; // TODO: Maybe use UnsafeArray directly? + + public EntityCommandBuffer(EntityManager entityManager) + { + _entityManager = entityManager; + _commands = new UnsafeList(32, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent); + } + + public void CreateEntity() + { + var command = new Command + { + type = CommandType.CreateEntity, + data = default, + entity = default, + componentTypeID = -1 + }; + + _commands.Add(command); + } + + public void DestroyEntity(Entity entity) + { + var command = new Command + { + type = CommandType.DestroyEntity, + data = default, + entity = entity, + componentTypeID = -1 + }; + + _commands.Add(command); + } + + public void AddComponent(Entity entity, T component) + where T : unmanaged, IComponent + { + var data = new UnsafeArray(sizeof(T), Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent); + MemoryUtility.MemCpy(&component, data.GetUnsafePtr(), (nuint)sizeof(T)); + + var command = new Command + { + type = CommandType.AddComponent, + data = data, + entity = entity, + componentTypeID = ComponentTypeID.value + }; + + _commands.Add(command); + } + + public void RemoveComponent(Entity entity) + where T : unmanaged, IComponent + { + var command = new Command + { + type = CommandType.RemoveComponent, + data = default, + entity = entity, + componentTypeID = ComponentTypeID.value + }; + + _commands.Add(command); + } + + public void Reset() + { + foreach (ref var command in _commands) + { + command.data.Dispose(); + } + + _commands.Clear(); + } + + public void Dispose() + { + foreach (ref var command in _commands) + { + command.data.Dispose(); + } + + _commands.Dispose(); + } +} diff --git a/Ghost.Entities/EntityManager.cs b/Ghost.Entities/EntityManager.cs index 48f7da6..ec0b37f 100644 --- a/Ghost.Entities/EntityManager.cs +++ b/Ghost.Entities/EntityManager.cs @@ -1,360 +1,266 @@ using Ghost.Core; -using Ghost.Entities.Components; -using Ghost.Entities.Query; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Utilities; +using System.Diagnostics; namespace Ghost.Entities; -public readonly struct EntityManager : IDisposable +public unsafe class EntityManager : IDisposable { - private readonly List _entities; - private readonly Queue _freeEntitySlots; + private struct EntityLocation + { + public Identifier archetypeID; + public int chunkIndex; + public int rowIndex; + } - private readonly World _world; - - public readonly int EntityCount => _entities.Count; - public readonly ReadOnlySpan Entities => CollectionsMarshal.AsSpan(_entities); + private World _world; + private UnsafeSlotMap _entityLocations; internal EntityManager(World world, int initialCapacity) { - _entities = new(initialCapacity); - _freeEntitySlots = new(initialCapacity); _world = world; + _entityLocations = new UnsafeSlotMap(initialCapacity, Allocator.Persistent); } - /// - /// Adds a new to the world. - /// - /// The created . - public readonly Entity CreateEntity() + internal ResultStatus UpdateEntityLocation(Entity entity, Identifier newArchetypeID, int newChunkIndex, int newRowIndex) { - Entity entity; - if (_freeEntitySlots.TryDequeue(out var id)) + ref var location = ref _entityLocations.GetElementReferenceAt(entity.ID, entity.Generation, out var exist); + if (!exist) { - entity = _entities[id]; + return ResultStatus.NotFound; } - else + + location.archetypeID = newArchetypeID; + location.chunkIndex = newChunkIndex; + location.rowIndex = newRowIndex; + + return ResultStatus.Success; + } + + public Entity CreateEntity(params ReadOnlySpan> componentTypeIDs) + { + var signatureHash = ComponentRegister.GetHashCode(componentTypeIDs); + var arcID = _world.GetArchetypeIDBySignatureHash(signatureHash); + + if (arcID.IsNotValid) { - id = _entities.Count; - entity = new Entity(id, 0); - _entities.Add(entity); + arcID = _world.CreateArchetype(componentTypeIDs, signatureHash); } + ref var archetype = ref _world.GetArchetypeReference(arcID); + archetype.AllocateEntity(out var chunkIndex, out var rowIndex); + + var id = _entityLocations.Add(new EntityLocation + { + archetypeID = arcID, + chunkIndex = chunkIndex, + rowIndex = rowIndex + }, out var generation); + + var entity = new Entity(id, generation); + archetype.SetEntity(chunkIndex, rowIndex, entity); + return entity; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal readonly void AddEntityInternal(Entity entity) + public Entity CreateEntity() { - _entities.Add(entity); + // Put into empty archetype + ref var emptyArchetype = ref _world.GetArchetypeReference(World.EmptyArchetypeID); + emptyArchetype.AllocateEntity(out var chunkIndex, out var rowIndex); + + var id = _entityLocations.Add(new EntityLocation + { + archetypeID = World.EmptyArchetypeID, + chunkIndex = chunkIndex, + rowIndex = rowIndex + }, out var generation); + + var entity = new Entity(id, generation); + emptyArchetype.SetEntity(chunkIndex, rowIndex, entity); + + return entity; } - /// - /// Removes the specified from the world. - /// - /// - public readonly void RemoveEntity(ref Entity entity) + public ResultStatus DestoryEntity(Entity entity) { - if (entity.ID >= _entities.Count || _entities[entity.ID].Generation != entity.Generation) + if (!_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location)) { - return; + return ResultStatus.NotFound; } - _world.ComponentStorage.Remove(entity); - - var slot = _entities[entity.ID]; - slot.IncrementGeneration(); - _entities[entity.ID] = slot; - _freeEntitySlots.Enqueue(entity.ID); - - entity = Entity.Invalid; - } - - /// - /// Checks if the given is valid and belongs to this . - /// - /// The entity to check. - /// True if the entity is valid and belongs to this world; otherwise, false. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool HasEntity(Entity entity) - { - if (!entity.IsValid - || entity.ID >= _entities.Count) + ref var archetype = ref _world.GetArchetypeReference(location.archetypeID); + var r = archetype.RemoveEntity(location.chunkIndex, location.rowIndex); + if (r != ResultStatus.Success) { - return false; + return r; } - return _entities[entity.ID].Generation == entity.Generation; - } - - /// - /// Adds a component of the specified type to the given entity. - /// - /// - /// This method use reflection to determine the type of the component being added. Use generic as much as possible. - /// - /// The entity to which the component will be added. - /// The component data to associate with the entity. - /// The type of the component being added. This must match the type of . - public readonly void AddComponent(Entity entity, IComponentData component, Type componentType) - { - _world.ComponentStorage.GetOrCreateComponentPool(componentType).Add(entity, component); - _world.ComponentStorage.GetOrCreateMask(componentType).SetBit(entity.ID); - } - - /// - /// Adds a component of type to the given . - /// - /// The type of the component to set. - /// The entity for which the component is to be add. - /// The component Value to add. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void AddComponent(Entity entity, T component) - where T : unmanaged, IComponentData - { - _world.ComponentStorage.GetOrCreateComponentPool().Add(entity, component); - _world.ComponentStorage.GetOrCreateMask().SetBit(entity.ID); - } - - /// - /// Removes a component of type from the given . - /// - /// The type of the component to remove. - /// The entity for which the component is to be remove. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool RemoveComponent(Entity entity) - where T : unmanaged, IComponentData - { - if (!_world.ComponentStorage.TryGetPool(out var pool) || !pool.Has(entity)) + if (!_entityLocations.Remove(entity.ID, entity.Generation)) { - return false; + return ResultStatus.NotFound; } - if (!pool.Remove(entity)) - { - return false; - } - - _world.ComponentStorage.GetOrCreateMask().ClearBit(entity.ID); - - return true; + return ResultStatus.Success; } - /// - /// Sets a component of the specified type for the given . - /// - /// The entity for which the component is to be set. - /// The component Value to set. - /// The type of the component to set. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void SetComponent(Entity entity, IComponentData component, Type type) + private static void CopyData(ref Archetype oldArch, int oldChunk, int oldRow, + ref Archetype newArch, int newChunk, int newRow) { - var typeHandle = TypeHandle.Get(type); - if (!_world.ComponentStorage.TryGetPool(typeHandle, out var pool)) + // Iterate every component type in the OLD archetype + for (var i = 0; i < oldArch.Layouts.Count; i++) { - return; - } + var layout = oldArch.Layouts[i]; - if (!pool.Has(entity)) - { - return; - } + 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); - pool.Set(entity, component); - } - - /// - /// Sets a component of type for the given . - /// - /// The type of the component to set. - /// The entity for which the component is to be set. - /// The component Value to set. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void SetComponent(Entity entity, in T component) - where T : unmanaged, IComponentData - { - _world.ComponentStorage.GetOrCreateComponentPool().Set(entity, in component); - } - - /// - /// Checks if the given has a component of the specified type. - /// - /// The entity to check. - /// The handle of the component type. - /// True if the entity has the component; otherwise, false. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool HasComponent(Entity entity, TypeHandle typeHandle) - { - return _world.ComponentStorage.TryGetMask(typeHandle, out var bitSet) && bitSet.Value.IsSet(entity.ID); - } - - /// - /// Retrieves a reference to a component of type associated with the given . - /// - /// The type of the component to retrieve. - /// The entity whose component is to be retrieved. - /// A to the component, or a null reference if the component does not exist. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly CompRef GetComponent(Entity entity) - where T : unmanaged, IComponentData - { - if (_world.ComponentStorage.TryGetPool(out var pool) && pool.Has(entity)) - { - return new CompRef(ref pool.GetRef(entity)); - } - else - { - return new CompRef(ref Unsafe.NullRef(), false); + MemoryUtility.MemCpy(src, dst, (nuint)layout.size); } } - /// - /// Retrieves all components associated with the specified entity. - /// - /// This method iterates through all available component pools to find components associated - /// with the given entity. It is designed to lazily yield components, making it efficient for scenarios where only - /// a subset of components may be needed. - /// The entity for which components are to be retrieved. - /// An enumerable collection of components associated with the specified entity. If the entity has no components, - /// the collection will be empty. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly IEnumerable GetComponents(Entity entity) + public ResultStatus AddComponent(Entity entity, Identifier componentID, void* component) { - foreach (var pool in _world.ComponentStorage.ComponentPools) + // Find current location + ref var location = ref _entityLocations.GetElementReferenceAt(entity.ID, entity.Generation, out var exist); + if (!exist) { - if (pool == null) + return ResultStatus.NotFound; + } + + // Build new archetype signature + ref var oldArchetype = ref _world.GetArchetypeReference(location.archetypeID); + var oldSignature = oldArchetype.Signature; + + // TODO: Check edge cache first. + var newArcID = oldArchetype.GetEdgeAdd(componentID); + if (newArcID.IsNotValid) + { + var largestComponentID = Math.Max(oldSignature.Count, componentID); + var length = UnsafeBitSet.RequiredLength(largestComponentID + 1); + + Span bits = stackalloc uint[length]; + bits.Clear(); + + var newSignature = new SpanBitSet(bits); + + var iterator = 0; + var compCount = 0; + while (true) { - continue; + var bit = oldSignature.NextSetBit(iterator); + if (bit == -1) + { + break; + } + + newSignature.SetBit(bit); + iterator = bit + 1; + compCount++; } - if (pool.Has(entity)) + compCount++; + newSignature.SetBit(componentID); + + // Find or create new archetype + var newSignatureHash = newSignature.GetHashCode(); + newArcID = _world.GetArchetypeIDBySignatureHash(newSignatureHash); + if (newArcID.IsNotValid) { - yield return pool.Get(entity); + // Create new archetype + Span> componentTypeIDs = stackalloc Identifier[compCount]; + componentTypeIDs[0] = componentID; + + iterator = 0; + while (true) + { + var bit = oldSignature.NextSetBit(iterator); + if (bit == -1) + { + break; + } + + componentTypeIDs[--compCount] = bit; + iterator = bit + 1; + } + + newArcID = _world.CreateArchetype(componentTypeIDs, newSignatureHash); } + + oldArchetype.AddEdgeAdd(componentID, newArcID); } + + // Move entity data + ref var newArchetype = ref _world.GetArchetypeReference(newArcID); + newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex); + CopyData(ref oldArchetype, location.chunkIndex, location.rowIndex, + ref newArchetype, newChunkIndex, newRowIndex); + + newArchetype.SetEntity(newChunkIndex, newRowIndex, entity); + newArchetype.SetComponentData(newChunkIndex, newRowIndex, componentID, component); + + var r = oldArchetype.RemoveEntity(location.chunkIndex, location.rowIndex); + Debug.Assert(r == ResultStatus.Success); // We assert it because the entity should exist if the whole system is consistent. + // if (r != ResultStatus.Success) + // { + // return r; + // } + + // Update location + location.archetypeID = newArcID; + location.chunkIndex = newChunkIndex; + location.rowIndex = newRowIndex; + + return ResultStatus.Success; } - /// - /// Retrieves an enumerable collection of raw pointers to the components associated with the specified entity. - /// - /// This method provides direct access to the memory locations of components, bypassing type - /// safety. Use with caution, as improper handling of raw pointers can lead to undefined behavior or memory - /// corruption. Ensure that the entity is valid and exists within the current world context before calling this - /// method. - /// The entity whose components are to be retrieved. - /// An enumerable collection of representing the memory addresses of the components associated - /// with the specified entity. The collection will be empty if the entity has no associated components. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly IEnumerable<(TypeHandle, IntPtr)> GetComponentsUnsafe(Entity entity) + public ResultStatus AddComponent(Entity entity, ref T component) + where T : unmanaged, IComponent { - for (var i = 0; i < _world.ComponentStorage.ComponentPools.Count; i++) + return AddComponent(entity, ComponentTypeID.value, UnsafeUtility.AddressOf(ref component)); + } + + public ResultStatus SetComponentData(Entity entity, Identifier componentID, void* pComponent) + { + if (!_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location)) { - var pool = _world.ComponentStorage.ComponentPools[i]; - if (pool == null) - { - continue; - } - - if (pool.Has(entity)) - { - yield return (_world.ComponentStorage.GetComponentPoolType(i), pool.GetUnsafe(entity)); - } - } - } - - /// - /// Adds a script of type to the given . - /// - /// The type of the script to add. - /// The entity to which the script is to be added. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void AddScript(Entity entity) - where T : ScriptComponent, new() - { - _world.ComponentStorage.ScriptComponentPool.Add(entity, new T()); - } - - /// - /// Adds a script of the specified type to the given . - /// - /// The entity to which the script is to be added. - /// The type of the script to add. - /// Thrown if the specified type does not inherit from . - /// Thrown if the script instance could not be created. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void AddScript(Entity entity, Type type) - { - if (!typeof(ScriptComponent).IsAssignableFrom(type)) - { - throw new ArgumentException($"Type {type} must inherit from ScriptComponent.", nameof(type)); + return ResultStatus.NotFound; } - var instance = (ScriptComponent?)Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of {type}."); - _world.ComponentStorage.ScriptComponentPool.Add(entity, instance); + ref var archetype = ref _world.GetArchetypeReference(location.archetypeID); + archetype.SetComponentData(location.chunkIndex, location.rowIndex, componentID, pComponent); + + return ResultStatus.Success; } - /// - /// Removes a script of type from the given . - /// - /// The type of the script to remove. - /// The entity from which the script is to be removed. - /// True if the script was successfully removed; otherwise, false. - public readonly bool RemoveScript(Entity entity) - where T : ScriptComponent + public ResultStatus SetComponentData(Entity entity, ref T component) + where T : unmanaged, IComponent { - if (!_world.ComponentStorage.ScriptComponentPool.Remove(entity)) + return SetComponentData(entity, ComponentTypeID.value, UnsafeUtility.AddressOf(ref component)); + } + + public bool HasComponent(Entity entity, Identifier componentID) + { + if (!_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location)) { return false; } - return true; + ref var archetype = ref _world.GetArchetypeReference(location.archetypeID); + return archetype.HasComponent(componentID); } - /// - /// Removes a script at the specified index from the given . - /// - /// The entity from which the script is to be removed. - /// The index of the script to remove. - /// True if the script was successfully removed; otherwise, false. - public readonly bool RemoveScriptAt(Entity entity, int index) + public bool HasComponent(Entity entity) + where T : unmanaged, IComponent { - if (!_world.ComponentStorage.ScriptComponentPool.RemoveAt(entity, index)) - { - return false; - } - - return true; + return HasComponent(entity, ComponentTypeID.value); } - /// - /// Retrieves the first script of type associated with the given . - /// - /// The type of the script to retrieve. - /// The entity whose script is to be retrieved. - /// The script of type , or null if no such script exists. - public readonly T? GetScript(Entity entity) - where T : ScriptComponent + public void Dispose() { - return (T?)_world.ComponentStorage.ScriptComponentPool.GetAll(entity)? - .FirstOrDefault(script => script is T tScript); + _entityLocations.Dispose(); } - - /// - /// Retrieves all scripts of type associated with the given . - /// - /// The type of the scripts to retrieve. - /// The entity whose scripts are to be retrieved. - /// An enumerable of scripts of type . - public readonly IEnumerable GetScripts(Entity entity) - where T : ScriptComponent - { - return (IEnumerable?)_world.ComponentStorage.ScriptComponentPool.GetAll(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty(); - } - - public readonly void Dispose() - { - _entities.Clear(); - _freeEntitySlots.Clear(); - } -} \ No newline at end of file +} diff --git a/Ghost.Entities/EntityQuery.cs b/Ghost.Entities/EntityQuery.cs new file mode 100644 index 0000000..7def3aa --- /dev/null +++ b/Ghost.Entities/EntityQuery.cs @@ -0,0 +1,60 @@ +namespace Ghost.Entities; + +public unsafe class EntityQuery + where T1 : unmanaged, IComponent + where T2 : unmanaged, IComponent +{ + // The Cache Struct + struct ArchetypeCache + { + public Archetype Archetype; + public int Offset1; // Offset for T1 + public int Offset2; // Offset for T2 + } + + private List _cache = new(); + + internal void AddMatchingArchetype(Archetype archetype) + { + // We look up the offsets ONCE when the archetype is registered + int off1 = archetype.GetOffset(ComponentTypeID.value); + int off2 = archetype.GetOffset(ComponentTypeID.value); + + _cache.Add(new ArchetypeCache + { + Archetype = archetype, + Offset1 = off1, + Offset2 = off2 + }); + } + + // The Optimized Iteration Loop + public void ForEach(delegate* action) + { + foreach (var cache in _cache) + { + var archetype = cache.Archetype; + var offset1 = cache.Offset1; + var offset2 = cache.Offset2; + + // Iterate Chunks + for (int i = 0; i < archetype.ChunkCount; i++) + { + var chunk = archetype.GetChunkReference(i); + var chunkPtr = chunk.GetUnsafePtr(); + var count = chunk.Count; + + // POINTER MATH ONLY - NO LOOKUPS + // We use the pre-calculated integer offsets + T1* ptr1 = (T1*)(chunkPtr + offset1); + T2* ptr2 = (T2*)(chunkPtr + offset2); + + // The hot loop + for (int k = 0; k < count; k++) + { + action(ref ptr1[k], ref ptr2[k]); + } + } + } + } +} diff --git a/Ghost.Entities/Ghost.Entities.csproj b/Ghost.Entities/Ghost.Entities.csproj index 451e5e1..f572c66 100644 --- a/Ghost.Entities/Ghost.Entities.csproj +++ b/Ghost.Entities/Ghost.Entities.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -7,105 +7,6 @@ True - - True - - - - True - - - - - True - True - ForEach.tt - - - True - True - QueryItem.tt - - - True - True - QueryRefComponent.tt - - - True - True - World.Query.tt - - - - - - TextTemplatingFileGenerator - ForEach.cs - - - TextTemplatingFileGenerator - QueryEnumerable.cs - - - TextTemplatingFilePreprocessor - Helpers.cs - - - TextTemplatingFileGenerator - QueryItem.cs - - - TextTemplatingFileGenerator - QueryRefComponent.cs - - - TextTemplatingFileGenerator - World.Query.cs - - - - - - - - - - True - True - ForEach.tt - - - True - True - Helpers.tt - - - True - True - QueryEnumerable.tt - - - True - True - QueryItem.tt - - - True - True - QueryRefComponent.tt - - - True - True - World.Query.tt - - - - - - - diff --git a/Ghost.Entities/Template/ForEach.cs b/Ghost.Entities/Template/ForEach.cs deleted file mode 100644 index 347c1ac..0000000 --- a/Ghost.Entities/Template/ForEach.cs +++ /dev/null @@ -1,12 +0,0 @@ - - -namespace Ghost.Entities; - -public delegate void ForEach(ref T0 t0Component); -public delegate void ForEach(ref T0 t0Component, ref T1 t1Component); -public delegate void ForEach(ref T0 t0Component, ref T1 t1Component, ref T2 t2Component); -public delegate void ForEach(ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component); -public delegate void ForEach(ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component); -public delegate void ForEach(ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component, ref T5 t5Component); -public delegate void ForEach(ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component, ref T5 t5Component, ref T6 t6Component); -public delegate void ForEach(ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component, ref T5 t5Component, ref T6 t6Component, ref T7 t7Component); diff --git a/Ghost.Entities/World.cs b/Ghost.Entities/World.cs index 434da58..6ec802d 100644 --- a/Ghost.Entities/World.cs +++ b/Ghost.Entities/World.cs @@ -1,17 +1,16 @@ -using Ghost.Entities.Components; -using Ghost.Entities.Query; -using Ghost.Entities.Systems; -using System.Diagnostics; +using Ghost.Core; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Ghost.Entities; -// TODO: Archetype system for better performance public partial class World { - private static List s_worlds = new(4); - private static Queue s_freeWorldSlots = new(); + private static List s_worlds = new(4); + private static Queue> s_freeWorldSlots = new(); + + internal static Identifier EmptyArchetypeID => new Identifier(0); public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count; @@ -21,53 +20,63 @@ public partial class World { if (s_freeWorldSlots.TryDequeue(out var index)) { - s_worlds[index] = new World(index, entityCapacity); + s_worlds[index.value] = new World(index, entityCapacity); } else { - if (s_worlds.Count >= WorldID.MaxValue) - { - throw new InvalidOperationException("Maximum number of worlds reached"); - } - - index = (WorldID)s_worlds.Count; + index = new Identifier(s_worlds.Count); s_worlds.Add(new World(index, entityCapacity)); } - return s_worlds[index]; + return s_worlds[index.value]!; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static World GetWorld(int index) + public static Result GetWorld(Identifier id) { - return s_worlds[index]; + if (id.value < 0 || id.value >= s_worlds.Count) + { + return Result.Create(default(World)!, ResultStatus.NotFound); + } + + var world = s_worlds[id.value]; + if (world is null) + { + return Result.Create(default(World)!, ResultStatus.NotFound); + } + + return Result.Create(world, ResultStatus.Success); } } -public partial class World : IDisposable, IEquatable +public partial class World : IIdentifierType, IDisposable, IEquatable { - private readonly WorldID _id; - private readonly EntityManager _entityManager; - private readonly ComponentStorage _componentStorage; - private readonly SystemStorage _systemStorage; + private readonly Identifier _id; - private bool _isDisposed = false; + private UnsafeList _archetypes; + private UnsafeHashMap> _archetypeLookup; // Signature Hash to Archetype ID + private EntityManager _entityManager; + private EntityCommandBuffer _entityCommandBuffer; - internal ComponentStorage ComponentStorage => _componentStorage; + private bool _disposed = false; - public WorldID ID => _id; + public Identifier ID => _id; public EntityManager EntityManager => _entityManager; - public SystemStorage SystemStorage => _systemStorage; + public EntityCommandBuffer EntityCommandBuffer => _entityCommandBuffer; - public event Action? ComponentChanged; - - private World(WorldID id, int entityCapacity) + private World(Identifier id, int entityCapacity) { _id = id; + + _archetypes = new UnsafeList(16, Allocator.Persistent); + _archetypeLookup = new UnsafeHashMap>(16, Allocator.Persistent); + _entityManager = new EntityManager(this, entityCapacity); - _componentStorage = new ComponentStorage(this); - _systemStorage = new SystemStorage(this); + _entityCommandBuffer = new EntityCommandBuffer(_entityManager); + + // Create the empty archetype + CreateArchetype(ReadOnlySpan>.Empty, 0); } ~World() @@ -75,53 +84,33 @@ public partial class World : IDisposable, IEquatable Dispose(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public CompRef GetSingleton() - where T : unmanaged, IComponentData + internal Identifier CreateArchetype(ReadOnlySpan> componentTypeIDs, int signatureHash) { - ref var component = ref CollectionsMarshal.GetValueRefOrAddDefault(SingletonContainer.container, _id, out _); - return new CompRef(ref component); + var arcID = new Identifier(_archetypes.Count); + _archetypes.Add(new Archetype(arcID, _id, componentTypeIDs)); + _archetypeLookup.Add(signatureHash, arcID); + + return arcID; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerable QueryScript() + internal ref Archetype GetArchetypeReference(Identifier id) { - if (_componentStorage.ScriptComponentPool.IsInitialized) + return ref _archetypes[id.value]; + } + + internal Identifier GetArchetypeIDBySignatureHash(int signatureHash) + { + if (_archetypeLookup.TryGetValue(signatureHash, out var arcID)) { - return _componentStorage.ScriptComponentPool.ExecutionList!; + return arcID; } - return Enumerable.Empty(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Conditional("GHOST_EDITOR")] - public void NotifyComponentChanged(Entity entity, Type type) - { - ComponentChanged?.Invoke(this, entity, type); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Conditional("GHOST_EDITOR")] - public void NotifyComponentChanged(Entity entity) - where T : unmanaged, IComponentData - { - NotifyComponentChanged(entity, typeof(T)); + return Identifier.Invalid; } public bool Equals(World? other) { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return _id == other._id; + return other is not null && _id == other._id; } public override int GetHashCode() @@ -146,19 +135,21 @@ public partial class World : IDisposable, IEquatable public void Dispose() { - if (_isDisposed) + if (_disposed) { return; } _entityManager.Dispose(); - _componentStorage.Dispose(); - _systemStorage.Dispose(); + + _archetypes.Dispose(); + _archetypeLookup.Dispose(); s_freeWorldSlots.Enqueue(_id); - _isDisposed = true; + _disposed = true; GC.SuppressFinalize(this); } -} \ No newline at end of file +} + diff --git a/Ghost.Graphics/Ghost.Graphics.csproj b/Ghost.Graphics/Ghost.Graphics.csproj index 96697ce..64b8d0b 100644 --- a/Ghost.Graphics/Ghost.Graphics.csproj +++ b/Ghost.Graphics/Ghost.Graphics.csproj @@ -52,10 +52,4 @@ - - - ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net10.0\Misaki.HighPerformance.LowLevel.dll - - - diff --git a/Ghost.SparseEntities/AssemblyInfo.cs b/Ghost.SparseEntities/AssemblyInfo.cs new file mode 100644 index 0000000..6c605e9 --- /dev/null +++ b/Ghost.SparseEntities/AssemblyInfo.cs @@ -0,0 +1,12 @@ +global using EntityID = System.Int32; +global using GenerationID = System.UInt16; +global using WorldID = System.UInt16; + +using Ghost.Core.Attributes; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Ghost.Engine")] +[assembly: InternalsVisibleTo("Ghost.Editor.Core")] +[assembly: InternalsVisibleTo("Ghost.Entities.Test")] + +[assembly: EngineAssembly] \ No newline at end of file diff --git a/Ghost.Entities/Components/ComponentStorage.cs b/Ghost.SparseEntities/Components/ComponentStorage.cs similarity index 99% rename from Ghost.Entities/Components/ComponentStorage.cs rename to Ghost.SparseEntities/Components/ComponentStorage.cs index 8861a34..c2319b1 100644 --- a/Ghost.Entities/Components/ComponentStorage.cs +++ b/Ghost.SparseEntities/Components/ComponentStorage.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Ghost.Entities.Components; +namespace Ghost.SparseEntities.Components; internal static class SingletonContainer where T : unmanaged, IComponentData diff --git a/Ghost.SparseEntities/Components/IComponentData.cs b/Ghost.SparseEntities/Components/IComponentData.cs new file mode 100644 index 0000000..7afcf83 --- /dev/null +++ b/Ghost.SparseEntities/Components/IComponentData.cs @@ -0,0 +1,5 @@ +namespace Ghost.SparseEntities.Components; + +public interface IComponentData +{ +} \ No newline at end of file diff --git a/Ghost.Entities/Components/ScriptComponent.cs b/Ghost.SparseEntities/Components/ScriptComponent.cs similarity index 98% rename from Ghost.Entities/Components/ScriptComponent.cs rename to Ghost.SparseEntities/Components/ScriptComponent.cs index 2fb3e18..950fee1 100644 --- a/Ghost.Entities/Components/ScriptComponent.cs +++ b/Ghost.SparseEntities/Components/ScriptComponent.cs @@ -1,4 +1,4 @@ -namespace Ghost.Entities.Components; +namespace Ghost.SparseEntities.Components; public abstract class ScriptComponent : IComponentData { diff --git a/Ghost.SparseEntities/Entity.cs b/Ghost.SparseEntities/Entity.cs new file mode 100644 index 0000000..df07cd0 --- /dev/null +++ b/Ghost.SparseEntities/Entity.cs @@ -0,0 +1,82 @@ +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; + +namespace Ghost.SparseEntities; + +[SkipLocalsInit] +public struct Entity : IEquatable, IComparable +{ + public const EntityID INVALID_ID = -1; + + [JsonInclude] + private EntityID _id; + private GenerationID _generation; + + public readonly EntityID ID + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _id; + } + + public readonly GenerationID Generation + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _generation; + } + + public readonly bool IsValid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ID != INVALID_ID; + } + + public static Entity Invalid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(INVALID_ID, GenerationID.MaxValue); + } + + internal Entity(EntityID id, GenerationID generation) + { + _id = id; + _generation = generation; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void IncrementGeneration() => _generation++; + + public readonly bool Equals(Entity other) + { + return _id == other._id && _generation == other._generation; + } + + public readonly int CompareTo(Entity other) + { + return _id.CompareTo(other._id); + } + + public override readonly bool Equals(object? obj) + { + return obj is Entity other && Equals(other); + } + + public override readonly int GetHashCode() + { + return _id.GetHashCode(); + } + + public static bool operator ==(Entity left, Entity right) + { + return left.Equals(right); + } + + public static bool operator !=(Entity left, Entity right) + { + return !(left == right); + } + + public override readonly string ToString() + { + return $"Entity {{ Index: {ID}, Generation: {Generation} }}"; + } +} \ No newline at end of file diff --git a/Ghost.SparseEntities/EntityManager.cs b/Ghost.SparseEntities/EntityManager.cs new file mode 100644 index 0000000..2a68e9a --- /dev/null +++ b/Ghost.SparseEntities/EntityManager.cs @@ -0,0 +1,360 @@ +using Ghost.Core; +using Ghost.SparseEntities.Components; +using Ghost.SparseEntities.Query; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ghost.SparseEntities; + +public readonly struct EntityManager : IDisposable +{ + private readonly List _entities; + private readonly Queue _freeEntitySlots; + + private readonly World _world; + + public readonly int EntityCount => _entities.Count; + public readonly ReadOnlySpan Entities => CollectionsMarshal.AsSpan(_entities); + + internal EntityManager(World world, int initialCapacity) + { + _entities = new(initialCapacity); + _freeEntitySlots = new(initialCapacity); + _world = world; + } + + /// + /// Adds a new to the world. + /// + /// The created . + public readonly Entity CreateEntity() + { + Entity entity; + if (_freeEntitySlots.TryDequeue(out var id)) + { + entity = _entities[id]; + } + else + { + id = _entities.Count; + entity = new Entity(id, 0); + _entities.Add(entity); + } + + return entity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly void AddEntityInternal(Entity entity) + { + _entities.Add(entity); + } + + /// + /// Removes the specified from the world. + /// + /// + public readonly void RemoveEntity(ref Entity entity) + { + if (entity.ID >= _entities.Count || _entities[entity.ID].Generation != entity.Generation) + { + return; + } + + _world.ComponentStorage.Remove(entity); + + var slot = _entities[entity.ID]; + slot.IncrementGeneration(); + _entities[entity.ID] = slot; + _freeEntitySlots.Enqueue(entity.ID); + + entity = Entity.Invalid; + } + + /// + /// Checks if the given is valid and belongs to this . + /// + /// The entity to check. + /// True if the entity is valid and belongs to this world; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool HasEntity(Entity entity) + { + if (!entity.IsValid + || entity.ID >= _entities.Count) + { + return false; + } + + return _entities[entity.ID].Generation == entity.Generation; + } + + /// + /// Adds a component of the specified type to the given entity. + /// + /// + /// This method use reflection to determine the type of the component being added. Use generic as much as possible. + /// + /// The entity to which the component will be added. + /// The component data to associate with the entity. + /// The type of the component being added. This must match the type of . + public readonly void AddComponent(Entity entity, IComponentData component, Type componentType) + { + _world.ComponentStorage.GetOrCreateComponentPool(componentType).Add(entity, component); + _world.ComponentStorage.GetOrCreateMask(componentType).SetBit(entity.ID); + } + + /// + /// Adds a component of type to the given . + /// + /// The type of the component to set. + /// The entity for which the component is to be add. + /// The component Value to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void AddComponent(Entity entity, T component) + where T : unmanaged, IComponentData + { + _world.ComponentStorage.GetOrCreateComponentPool().Add(entity, component); + _world.ComponentStorage.GetOrCreateMask().SetBit(entity.ID); + } + + /// + /// Removes a component of type from the given . + /// + /// The type of the component to remove. + /// The entity for which the component is to be remove. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool RemoveComponent(Entity entity) + where T : unmanaged, IComponentData + { + if (!_world.ComponentStorage.TryGetPool(out var pool) || !pool.Has(entity)) + { + return false; + } + + if (!pool.Remove(entity)) + { + return false; + } + + _world.ComponentStorage.GetOrCreateMask().ClearBit(entity.ID); + + return true; + } + + /// + /// Sets a component of the specified type for the given . + /// + /// The entity for which the component is to be set. + /// The component Value to set. + /// The type of the component to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void SetComponent(Entity entity, IComponentData component, Type type) + { + var typeHandle = TypeHandle.Get(type); + if (!_world.ComponentStorage.TryGetPool(typeHandle, out var pool)) + { + return; + } + + if (!pool.Has(entity)) + { + return; + } + + pool.Set(entity, component); + } + + /// + /// Sets a component of type for the given . + /// + /// The type of the component to set. + /// The entity for which the component is to be set. + /// The component Value to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void SetComponent(Entity entity, in T component) + where T : unmanaged, IComponentData + { + _world.ComponentStorage.GetOrCreateComponentPool().Set(entity, in component); + } + + /// + /// Checks if the given has a component of the specified type. + /// + /// The entity to check. + /// The handle of the component type. + /// True if the entity has the component; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool HasComponent(Entity entity, TypeHandle typeHandle) + { + return _world.ComponentStorage.TryGetMask(typeHandle, out var bitSet) && bitSet.Value.IsSet(entity.ID); + } + + /// + /// Retrieves a reference to a component of type associated with the given . + /// + /// The type of the component to retrieve. + /// The entity whose component is to be retrieved. + /// A to the component, or a null reference if the component does not exist. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly CompRef GetComponent(Entity entity) + where T : unmanaged, IComponentData + { + if (_world.ComponentStorage.TryGetPool(out var pool) && pool.Has(entity)) + { + return new CompRef(ref pool.GetRef(entity)); + } + else + { + return new CompRef(ref Unsafe.NullRef(), false); + } + } + + /// + /// Retrieves all components associated with the specified entity. + /// + /// This method iterates through all available component pools to find components associated + /// with the given entity. It is designed to lazily yield components, making it efficient for scenarios where only + /// a subset of components may be needed. + /// The entity for which components are to be retrieved. + /// An enumerable collection of components associated with the specified entity. If the entity has no components, + /// the collection will be empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly IEnumerable GetComponents(Entity entity) + { + foreach (var pool in _world.ComponentStorage.ComponentPools) + { + if (pool == null) + { + continue; + } + + if (pool.Has(entity)) + { + yield return pool.Get(entity); + } + } + } + + /// + /// Retrieves an enumerable collection of raw pointers to the components associated with the specified entity. + /// + /// This method provides direct access to the memory locations of components, bypassing type + /// safety. Use with caution, as improper handling of raw pointers can lead to undefined behavior or memory + /// corruption. Ensure that the entity is valid and exists within the current world context before calling this + /// method. + /// The entity whose components are to be retrieved. + /// An enumerable collection of representing the memory addresses of the components associated + /// with the specified entity. The collection will be empty if the entity has no associated components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly IEnumerable<(TypeHandle, IntPtr)> GetComponentsUnsafe(Entity entity) + { + for (var i = 0; i < _world.ComponentStorage.ComponentPools.Count; i++) + { + var pool = _world.ComponentStorage.ComponentPools[i]; + if (pool == null) + { + continue; + } + + if (pool.Has(entity)) + { + yield return (_world.ComponentStorage.GetComponentPoolType(i), pool.GetUnsafe(entity)); + } + } + } + + /// + /// Adds a script of type to the given . + /// + /// The type of the script to add. + /// The entity to which the script is to be added. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void AddScript(Entity entity) + where T : ScriptComponent, new() + { + _world.ComponentStorage.ScriptComponentPool.Add(entity, new T()); + } + + /// + /// Adds a script of the specified type to the given . + /// + /// The entity to which the script is to be added. + /// The type of the script to add. + /// Thrown if the specified type does not inherit from . + /// Thrown if the script instance could not be created. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void AddScript(Entity entity, Type type) + { + if (!typeof(ScriptComponent).IsAssignableFrom(type)) + { + throw new ArgumentException($"Type {type} must inherit from ScriptComponent.", nameof(type)); + } + + var instance = (ScriptComponent?)Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of {type}."); + _world.ComponentStorage.ScriptComponentPool.Add(entity, instance); + } + + /// + /// Removes a script of type from the given . + /// + /// The type of the script to remove. + /// The entity from which the script is to be removed. + /// True if the script was successfully removed; otherwise, false. + public readonly bool RemoveScript(Entity entity) + where T : ScriptComponent + { + if (!_world.ComponentStorage.ScriptComponentPool.Remove(entity)) + { + return false; + } + + return true; + } + + /// + /// Removes a script at the specified index from the given . + /// + /// The entity from which the script is to be removed. + /// The index of the script to remove. + /// True if the script was successfully removed; otherwise, false. + public readonly bool RemoveScriptAt(Entity entity, int index) + { + if (!_world.ComponentStorage.ScriptComponentPool.RemoveAt(entity, index)) + { + return false; + } + + return true; + } + + /// + /// Retrieves the first script of type associated with the given . + /// + /// The type of the script to retrieve. + /// The entity whose script is to be retrieved. + /// The script of type , or null if no such script exists. + public readonly T? GetScript(Entity entity) + where T : ScriptComponent + { + return (T?)_world.ComponentStorage.ScriptComponentPool.GetAll(entity)? + .FirstOrDefault(script => script is T tScript); + } + + /// + /// Retrieves all scripts of type associated with the given . + /// + /// The type of the scripts to retrieve. + /// The entity whose scripts are to be retrieved. + /// An enumerable of scripts of type . + public readonly IEnumerable GetScripts(Entity entity) + where T : ScriptComponent + { + return (IEnumerable?)_world.ComponentStorage.ScriptComponentPool.GetAll(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty(); + } + + public readonly void Dispose() + { + _entities.Clear(); + _freeEntitySlots.Clear(); + } +} \ No newline at end of file diff --git a/Ghost.SparseEntities/Ghost.SparseEntities.csproj b/Ghost.SparseEntities/Ghost.SparseEntities.csproj new file mode 100644 index 0000000..451e5e1 --- /dev/null +++ b/Ghost.SparseEntities/Ghost.SparseEntities.csproj @@ -0,0 +1,113 @@ + + + + net10.0 + enable + enable + True + + + + True + + + + True + + + + + True + True + ForEach.tt + + + True + True + QueryItem.tt + + + True + True + QueryRefComponent.tt + + + True + True + World.Query.tt + + + + + + TextTemplatingFileGenerator + ForEach.cs + + + TextTemplatingFileGenerator + QueryEnumerable.cs + + + TextTemplatingFilePreprocessor + Helpers.cs + + + TextTemplatingFileGenerator + QueryItem.cs + + + TextTemplatingFileGenerator + QueryRefComponent.cs + + + TextTemplatingFileGenerator + World.Query.cs + + + + + + + + + + True + True + ForEach.tt + + + True + True + Helpers.tt + + + True + True + QueryEnumerable.tt + + + True + True + QueryItem.tt + + + True + True + QueryRefComponent.tt + + + True + True + World.Query.tt + + + + + + + + + + + + diff --git a/Ghost.Entities/Query/QueryBuilder.cs b/Ghost.SparseEntities/Query/QueryBuilder.cs similarity index 94% rename from Ghost.Entities/Query/QueryBuilder.cs rename to Ghost.SparseEntities/Query/QueryBuilder.cs index d822142..3e7d6ad 100644 --- a/Ghost.Entities/Query/QueryBuilder.cs +++ b/Ghost.SparseEntities/Query/QueryBuilder.cs @@ -1,6 +1,6 @@ using Ghost.Core; -namespace Ghost.Entities.Query; +namespace Ghost.SparseEntities.Query; public struct QueryBuilder { diff --git a/Ghost.Entities/Query/QueryFilter.cs b/Ghost.SparseEntities/Query/QueryFilter.cs similarity index 98% rename from Ghost.Entities/Query/QueryFilter.cs rename to Ghost.SparseEntities/Query/QueryFilter.cs index 0acd9bc..69ece4e 100644 --- a/Ghost.Entities/Query/QueryFilter.cs +++ b/Ghost.SparseEntities/Query/QueryFilter.cs @@ -2,7 +2,7 @@ using Ghost.Core; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; -namespace Ghost.Entities.Query; +namespace Ghost.SparseEntities.Query; public struct QueryFilter : IDisposable { diff --git a/Ghost.Entities/Query/QueryTypeParameter.cs b/Ghost.SparseEntities/Query/QueryTypeParameter.cs similarity index 94% rename from Ghost.Entities/Query/QueryTypeParameter.cs rename to Ghost.SparseEntities/Query/QueryTypeParameter.cs index 4a329e6..9cc059a 100644 --- a/Ghost.Entities/Query/QueryTypeParameter.cs +++ b/Ghost.SparseEntities/Query/QueryTypeParameter.cs @@ -1,7 +1,7 @@ -using Ghost.Entities.Components; +using Ghost.SparseEntities.Components; using System.Runtime.CompilerServices; -namespace Ghost.Entities.Query; +namespace Ghost.SparseEntities.Query; public interface IQueryTypeParameter where T : IComponentData diff --git a/Ghost.Entities/Systems/ISystem.cs b/Ghost.SparseEntities/Systems/ISystem.cs similarity index 93% rename from Ghost.Entities/Systems/ISystem.cs rename to Ghost.SparseEntities/Systems/ISystem.cs index 8e8e659..b39ca6d 100644 --- a/Ghost.Entities/Systems/ISystem.cs +++ b/Ghost.SparseEntities/Systems/ISystem.cs @@ -1,4 +1,4 @@ -namespace Ghost.Entities.Systems; +namespace Ghost.SparseEntities.Systems; /// /// Attribute to declare that a system depends on one or more other systems. diff --git a/Ghost.Entities/Systems/SystemDependencyBuilder.cs b/Ghost.SparseEntities/Systems/SystemDependencyBuilder.cs similarity index 99% rename from Ghost.Entities/Systems/SystemDependencyBuilder.cs rename to Ghost.SparseEntities/Systems/SystemDependencyBuilder.cs index 73f7f6c..217c781 100644 --- a/Ghost.Entities/Systems/SystemDependencyBuilder.cs +++ b/Ghost.SparseEntities/Systems/SystemDependencyBuilder.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace Ghost.Entities.Systems; +namespace Ghost.SparseEntities.Systems; internal class SystemDependencyBuilder { diff --git a/Ghost.Entities/Systems/SystemState.cs b/Ghost.SparseEntities/Systems/SystemState.cs similarity index 68% rename from Ghost.Entities/Systems/SystemState.cs rename to Ghost.SparseEntities/Systems/SystemState.cs index e6722fb..a18d26e 100644 --- a/Ghost.Entities/Systems/SystemState.cs +++ b/Ghost.SparseEntities/Systems/SystemState.cs @@ -1,4 +1,4 @@ -namespace Ghost.Entities.Systems; +namespace Ghost.SparseEntities.Systems; public struct SystemState { diff --git a/Ghost.Entities/Systems/SystemStorage.cs b/Ghost.SparseEntities/Systems/SystemStorage.cs similarity index 98% rename from Ghost.Entities/Systems/SystemStorage.cs rename to Ghost.SparseEntities/Systems/SystemStorage.cs index e3a4fb3..d7ba08e 100644 --- a/Ghost.Entities/Systems/SystemStorage.cs +++ b/Ghost.SparseEntities/Systems/SystemStorage.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Ghost.Entities.Systems; +namespace Ghost.SparseEntities.Systems; [SkipLocalsInit] public readonly struct SystemStorage diff --git a/Ghost.SparseEntities/Template/ForEach.cs b/Ghost.SparseEntities/Template/ForEach.cs new file mode 100644 index 0000000..4251f6e --- /dev/null +++ b/Ghost.SparseEntities/Template/ForEach.cs @@ -0,0 +1,12 @@ + + +namespace Ghost.SparseEntities; + +public delegate void ForEach(ref T0 t0Component); +public delegate void ForEach(ref T0 t0Component,ref T1 t1Component); +public delegate void ForEach(ref T0 t0Component,ref T1 t1Component,ref T2 t2Component); +public delegate void ForEach(ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component); +public delegate void ForEach(ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component); +public delegate void ForEach(ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component,ref T5 t5Component); +public delegate void ForEach(ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component,ref T5 t5Component,ref T6 t6Component); +public delegate void ForEach(ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component,ref T5 t5Component,ref T6 t6Component,ref T7 t7Component); diff --git a/Ghost.Entities/Template/ForEach.tt b/Ghost.SparseEntities/Template/ForEach.tt similarity index 100% rename from Ghost.Entities/Template/ForEach.tt rename to Ghost.SparseEntities/Template/ForEach.tt diff --git a/Ghost.Entities/Template/Helpers.ttinclude b/Ghost.SparseEntities/Template/Helpers.ttinclude similarity index 100% rename from Ghost.Entities/Template/Helpers.ttinclude rename to Ghost.SparseEntities/Template/Helpers.ttinclude diff --git a/Ghost.Entities/Template/QueryEnumerable.cs b/Ghost.SparseEntities/Template/QueryEnumerable.cs similarity index 99% rename from Ghost.Entities/Template/QueryEnumerable.cs rename to Ghost.SparseEntities/Template/QueryEnumerable.cs index 065db17..ce321ef 100644 --- a/Ghost.Entities/Template/QueryEnumerable.cs +++ b/Ghost.SparseEntities/Template/QueryEnumerable.cs @@ -1,12 +1,12 @@ using Ghost.Core; -using Ghost.Entities.Components; -using Ghost.Entities.Query; +using Ghost.SparseEntities.Components; +using Ghost.SparseEntities.Query; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; -namespace Ghost.Entities; +namespace Ghost.SparseEntities; public unsafe ref struct QueryEnumerable where T0 : unmanaged, IComponentData diff --git a/Ghost.Entities/Template/QueryEnumerable.tt b/Ghost.SparseEntities/Template/QueryEnumerable.tt similarity index 100% rename from Ghost.Entities/Template/QueryEnumerable.tt rename to Ghost.SparseEntities/Template/QueryEnumerable.tt diff --git a/Ghost.Entities/Template/QueryItem.cs b/Ghost.SparseEntities/Template/QueryItem.cs similarity index 85% rename from Ghost.Entities/Template/QueryItem.cs rename to Ghost.SparseEntities/Template/QueryItem.cs index 33fca9e..fee73c5 100644 --- a/Ghost.Entities/Template/QueryItem.cs +++ b/Ghost.SparseEntities/Template/QueryItem.cs @@ -1,9 +1,9 @@ -using Ghost.Entities.Components; -using Ghost.Entities.Query; +using Ghost.SparseEntities.Components; +using Ghost.SparseEntities.Query; -namespace Ghost.Entities; +namespace Ghost.SparseEntities; public readonly struct QueryItem where T0 : unmanaged, IComponentData @@ -26,7 +26,7 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); } } @@ -54,8 +54,8 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); - c1 = new(ref _pool1.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); + c1 = new (ref _pool1.GetRef(_entity)); } } @@ -86,9 +86,9 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); - c1 = new(ref _pool1.GetRef(_entity)); - c2 = new(ref _pool2.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); + c1 = new (ref _pool1.GetRef(_entity)); + c2 = new (ref _pool2.GetRef(_entity)); } } @@ -122,10 +122,10 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); - c1 = new(ref _pool1.GetRef(_entity)); - c2 = new(ref _pool2.GetRef(_entity)); - c3 = new(ref _pool3.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); + c1 = new (ref _pool1.GetRef(_entity)); + c2 = new (ref _pool2.GetRef(_entity)); + c3 = new (ref _pool3.GetRef(_entity)); } } @@ -162,11 +162,11 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); - c1 = new(ref _pool1.GetRef(_entity)); - c2 = new(ref _pool2.GetRef(_entity)); - c3 = new(ref _pool3.GetRef(_entity)); - c4 = new(ref _pool4.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); + c1 = new (ref _pool1.GetRef(_entity)); + c2 = new (ref _pool2.GetRef(_entity)); + c3 = new (ref _pool3.GetRef(_entity)); + c4 = new (ref _pool4.GetRef(_entity)); } } @@ -206,12 +206,12 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); - c1 = new(ref _pool1.GetRef(_entity)); - c2 = new(ref _pool2.GetRef(_entity)); - c3 = new(ref _pool3.GetRef(_entity)); - c4 = new(ref _pool4.GetRef(_entity)); - c5 = new(ref _pool5.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); + c1 = new (ref _pool1.GetRef(_entity)); + c2 = new (ref _pool2.GetRef(_entity)); + c3 = new (ref _pool3.GetRef(_entity)); + c4 = new (ref _pool4.GetRef(_entity)); + c5 = new (ref _pool5.GetRef(_entity)); } } @@ -254,13 +254,13 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); - c1 = new(ref _pool1.GetRef(_entity)); - c2 = new(ref _pool2.GetRef(_entity)); - c3 = new(ref _pool3.GetRef(_entity)); - c4 = new(ref _pool4.GetRef(_entity)); - c5 = new(ref _pool5.GetRef(_entity)); - c6 = new(ref _pool6.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); + c1 = new (ref _pool1.GetRef(_entity)); + c2 = new (ref _pool2.GetRef(_entity)); + c3 = new (ref _pool3.GetRef(_entity)); + c4 = new (ref _pool4.GetRef(_entity)); + c5 = new (ref _pool5.GetRef(_entity)); + c6 = new (ref _pool6.GetRef(_entity)); } } @@ -306,14 +306,14 @@ public readonly struct QueryItem { entity = _entity; - c0 = new(ref _pool0.GetRef(_entity)); - c1 = new(ref _pool1.GetRef(_entity)); - c2 = new(ref _pool2.GetRef(_entity)); - c3 = new(ref _pool3.GetRef(_entity)); - c4 = new(ref _pool4.GetRef(_entity)); - c5 = new(ref _pool5.GetRef(_entity)); - c6 = new(ref _pool6.GetRef(_entity)); - c7 = new(ref _pool7.GetRef(_entity)); + c0 = new (ref _pool0.GetRef(_entity)); + c1 = new (ref _pool1.GetRef(_entity)); + c2 = new (ref _pool2.GetRef(_entity)); + c3 = new (ref _pool3.GetRef(_entity)); + c4 = new (ref _pool4.GetRef(_entity)); + c5 = new (ref _pool5.GetRef(_entity)); + c6 = new (ref _pool6.GetRef(_entity)); + c7 = new (ref _pool7.GetRef(_entity)); } } diff --git a/Ghost.Entities/Template/QueryItem.tt b/Ghost.SparseEntities/Template/QueryItem.tt similarity index 100% rename from Ghost.Entities/Template/QueryItem.tt rename to Ghost.SparseEntities/Template/QueryItem.tt diff --git a/Ghost.Entities/Template/QueryRefComponent.cs b/Ghost.SparseEntities/Template/QueryRefComponent.cs similarity index 69% rename from Ghost.Entities/Template/QueryRefComponent.cs rename to Ghost.SparseEntities/Template/QueryRefComponent.cs index 20b06da..0a12253 100644 --- a/Ghost.Entities/Template/QueryRefComponent.cs +++ b/Ghost.SparseEntities/Template/QueryRefComponent.cs @@ -1,22 +1,22 @@ -using Ghost.Entities.Components; +using Ghost.SparseEntities.Components; -namespace Ghost.Entities; +namespace Ghost.SparseEntities; public delegate void QueryRefComponent(Entity entity, ref T0 t0Component) where T0 : unmanaged, IComponentData; -public delegate void QueryRefComponent(Entity entity, ref T0 t0Component, ref T1 t1Component) +public delegate void QueryRefComponent(Entity entity, ref T0 t0Component,ref T1 t1Component) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData; -public delegate void QueryRefComponent(Entity entity, ref T0 t0Component, ref T1 t1Component, ref T2 t2Component) +public delegate void QueryRefComponent(Entity entity, ref T0 t0Component,ref T1 t1Component,ref T2 t2Component) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData; -public delegate void QueryRefComponent(Entity entity, ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component) +public delegate void QueryRefComponent(Entity entity, ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData; -public delegate void QueryRefComponent(Entity entity, ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component) +public delegate void QueryRefComponent(Entity entity, ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData; -public delegate void QueryRefComponent(Entity entity, ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component, ref T5 t5Component) +public delegate void QueryRefComponent(Entity entity, ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component,ref T5 t5Component) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData where T5 : unmanaged, IComponentData; -public delegate void QueryRefComponent(Entity entity, ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component, ref T5 t5Component, ref T6 t6Component) +public delegate void QueryRefComponent(Entity entity, ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component,ref T5 t5Component,ref T6 t6Component) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData where T5 : unmanaged, IComponentData where T6 : unmanaged, IComponentData; -public delegate void QueryRefComponent(Entity entity, ref T0 t0Component, ref T1 t1Component, ref T2 t2Component, ref T3 t3Component, ref T4 t4Component, ref T5 t5Component, ref T6 t6Component, ref T7 t7Component) +public delegate void QueryRefComponent(Entity entity, ref T0 t0Component,ref T1 t1Component,ref T2 t2Component,ref T3 t3Component,ref T4 t4Component,ref T5 t5Component,ref T6 t6Component,ref T7 t7Component) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData where T5 : unmanaged, IComponentData where T6 : unmanaged, IComponentData where T7 : unmanaged, IComponentData; diff --git a/Ghost.Entities/Template/QueryRefComponent.tt b/Ghost.SparseEntities/Template/QueryRefComponent.tt similarity index 100% rename from Ghost.Entities/Template/QueryRefComponent.tt rename to Ghost.SparseEntities/Template/QueryRefComponent.tt diff --git a/Ghost.Entities/Template/World.Query.cs b/Ghost.SparseEntities/Template/World.Query.cs similarity index 96% rename from Ghost.Entities/Template/World.Query.cs rename to Ghost.SparseEntities/Template/World.Query.cs index 10d97c2..055c9fb 100644 --- a/Ghost.Entities/Template/World.Query.cs +++ b/Ghost.SparseEntities/Template/World.Query.cs @@ -1,9 +1,9 @@ -using Ghost.Entities.Components; -using Ghost.Entities.Query; +using Ghost.SparseEntities.Components; +using Ghost.SparseEntities.Query; -namespace Ghost.Entities; +namespace Ghost.SparseEntities; public partial class World { @@ -21,7 +21,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0))) @@ -50,7 +50,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0) && _componentStorage.TryGetPool(out var pool1))) @@ -79,7 +79,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0) && _componentStorage.TryGetPool(out var pool1) && _componentStorage.TryGetPool(out var pool2))) @@ -108,7 +108,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0) && _componentStorage.TryGetPool(out var pool1) && _componentStorage.TryGetPool(out var pool2) && _componentStorage.TryGetPool(out var pool3))) @@ -137,7 +137,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0) && _componentStorage.TryGetPool(out var pool1) && _componentStorage.TryGetPool(out var pool2) && _componentStorage.TryGetPool(out var pool3) && _componentStorage.TryGetPool(out var pool4))) @@ -166,7 +166,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData where T5 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0) && _componentStorage.TryGetPool(out var pool1) && _componentStorage.TryGetPool(out var pool2) && _componentStorage.TryGetPool(out var pool3) && _componentStorage.TryGetPool(out var pool4) && _componentStorage.TryGetPool(out var pool5))) @@ -195,7 +195,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData where T5 : unmanaged, IComponentData where T6 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0) && _componentStorage.TryGetPool(out var pool1) && _componentStorage.TryGetPool(out var pool2) && _componentStorage.TryGetPool(out var pool3) && _componentStorage.TryGetPool(out var pool4) && _componentStorage.TryGetPool(out var pool5) && _componentStorage.TryGetPool(out var pool6))) @@ -224,7 +224,7 @@ public partial class World pool0.Count); } - public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) + public QueryEnumerable QueryFilter(ref readonly QueryFilter filter) where T0 : unmanaged, IComponentData where T1 : unmanaged, IComponentData where T2 : unmanaged, IComponentData where T3 : unmanaged, IComponentData where T4 : unmanaged, IComponentData where T5 : unmanaged, IComponentData where T6 : unmanaged, IComponentData where T7 : unmanaged, IComponentData { if (!(_componentStorage.TryGetPool(out var pool0) && _componentStorage.TryGetPool(out var pool1) && _componentStorage.TryGetPool(out var pool2) && _componentStorage.TryGetPool(out var pool3) && _componentStorage.TryGetPool(out var pool4) && _componentStorage.TryGetPool(out var pool5) && _componentStorage.TryGetPool(out var pool6) && _componentStorage.TryGetPool(out var pool7))) diff --git a/Ghost.Entities/Template/World.Query.tt b/Ghost.SparseEntities/Template/World.Query.tt similarity index 100% rename from Ghost.Entities/Template/World.Query.tt rename to Ghost.SparseEntities/Template/World.Query.tt diff --git a/Ghost.SparseEntities/World.cs b/Ghost.SparseEntities/World.cs new file mode 100644 index 0000000..64aa51a --- /dev/null +++ b/Ghost.SparseEntities/World.cs @@ -0,0 +1,164 @@ +using Ghost.SparseEntities.Components; +using Ghost.SparseEntities.Query; +using Ghost.SparseEntities.Systems; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ghost.SparseEntities; + +// TODO: Archetype system for better performance +public partial class World +{ + private static List s_worlds = new(4); + private static Queue s_freeWorldSlots = new(); + + public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count; + + public static World Create(int entityCapacity = 16) + { + lock (s_worlds) + { + if (s_freeWorldSlots.TryDequeue(out var index)) + { + s_worlds[index] = new World(index, entityCapacity); + } + else + { + if (s_worlds.Count >= WorldID.MaxValue) + { + throw new InvalidOperationException("Maximum number of worlds reached"); + } + + index = (WorldID)s_worlds.Count; + s_worlds.Add(new World(index, entityCapacity)); + } + + return s_worlds[index]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static World GetWorld(int index) + { + return s_worlds[index]; + } +} + +public partial class World : IDisposable, IEquatable +{ + private readonly WorldID _id; + private readonly EntityManager _entityManager; + private readonly ComponentStorage _componentStorage; + private readonly SystemStorage _systemStorage; + + private bool _isDisposed = false; + + internal ComponentStorage ComponentStorage => _componentStorage; + + public WorldID ID => _id; + public EntityManager EntityManager => _entityManager; + public SystemStorage SystemStorage => _systemStorage; + + public event Action? ComponentChanged; + + private World(WorldID id, int entityCapacity) + { + _id = id; + _entityManager = new EntityManager(this, entityCapacity); + _componentStorage = new ComponentStorage(this); + _systemStorage = new SystemStorage(this); + } + + ~World() + { + Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CompRef GetSingleton() + where T : unmanaged, IComponentData + { + ref var component = ref CollectionsMarshal.GetValueRefOrAddDefault(SingletonContainer.container, _id, out _); + return new CompRef(ref component); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerable QueryScript() + { + if (_componentStorage.ScriptComponentPool.IsInitialized) + { + return _componentStorage.ScriptComponentPool.ExecutionList!; + } + + return Enumerable.Empty(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Conditional("GHOST_EDITOR")] + public void NotifyComponentChanged(Entity entity, Type type) + { + ComponentChanged?.Invoke(this, entity, type); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Conditional("GHOST_EDITOR")] + public void NotifyComponentChanged(Entity entity) + where T : unmanaged, IComponentData + { + NotifyComponentChanged(entity, typeof(T)); + } + + public bool Equals(World? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return _id == other._id; + } + + public override int GetHashCode() + { + return _id.GetHashCode(); + } + + public override bool Equals(object? obj) + { + return obj is World other && Equals(other); + } + + public static bool operator ==(World? left, World? right) + { + return left?.Equals(right) ?? right is null; + } + + public static bool operator !=(World? left, World? right) + { + return !(left == right); + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _entityManager.Dispose(); + _componentStorage.Dispose(); + _systemStorage.Dispose(); + + s_freeWorldSlots.Enqueue(_id); + + _isDisposed = true; + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/GhostEngine.slnx b/GhostEngine.slnx index 0044d31..a2abc89 100644 --- a/GhostEngine.slnx +++ b/GhostEngine.slnx @@ -23,7 +23,8 @@ - + +