diff --git a/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs b/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs index 0744b2c..101771f 100644 --- a/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs +++ b/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs @@ -30,7 +30,6 @@ internal class WorldNodeSerializer : CustomSerializer writer.WriteObject(() => { writer.WriteString(Property.NAME, value.Name); - writer.WriteStartArray(Property.ENTITIES); for (var i = 0; i < value.World.ArchetypeCount; i++) @@ -52,7 +51,9 @@ internal class WorldNodeSerializer : CustomSerializer continue; } - writer.WriteStartObject(type.AssemblyQualifiedName); + writer.WriteStartObject(); + writer.WriteString("Type", type.AssemblyQualifiedName); + writer.WritePropertyName("Data"); var pComponentData = chunk.GetUnsafePtr() + layout.offset + (k * size); ComponentSerializerRegistry.SerializeJson(layout.componentID, writer, pComponentData, options); @@ -145,4 +146,4 @@ internal class WorldNodeSerializer : CustomSerializer { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/Ghost.Entities.Test/EntityQueryTest.cs b/Ghost.Entities.Test/EntityQueryTest.cs index 61aa4d4..a78fb78 100644 --- a/Ghost.Entities.Test/EntityQueryTest.cs +++ b/Ghost.Entities.Test/EntityQueryTest.cs @@ -98,9 +98,19 @@ public partial class EntityQueryTest : ITest public struct Transform : IEnableableComponent { public float3 position; + + public override string ToString() + { + return $"Position: {position}"; + } } public struct Mesh : IComponent { public int index; + + public override string ToString() + { + return $"Index: {index}"; + } } diff --git a/Ghost.Entities.Test/Ghost.Entities.Test.csproj b/Ghost.Entities.Test/Ghost.Entities.Test.csproj index 1089430..32c399e 100644 --- a/Ghost.Entities.Test/Ghost.Entities.Test.csproj +++ b/Ghost.Entities.Test/Ghost.Entities.Test.csproj @@ -5,8 +5,7 @@ net10.0 enable enable - True - True + True diff --git a/Ghost.Entities.Test/Program.cs b/Ghost.Entities.Test/Program.cs index c05eb79..efc5351 100644 --- a/Ghost.Entities.Test/Program.cs +++ b/Ghost.Entities.Test/Program.cs @@ -3,5 +3,5 @@ using Ghost.Test.Core; using Misaki.HighPerformance.LowLevel.Buffer; AllocationManager.EnableDebugLayer(); -TestRunner.Run(); +TestRunner.Run(); AllocationManager.Dispose(); diff --git a/Ghost.Entities.Test/SerializationTest.cs b/Ghost.Entities.Test/SerializationTest.cs new file mode 100644 index 0000000..6180c69 --- /dev/null +++ b/Ghost.Entities.Test/SerializationTest.cs @@ -0,0 +1,157 @@ +using Ghost.Test.Core; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.Mathematics; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Ghost.Entities.Test; + +public class SerializationTest : ITest +{ + private World _world = null!; + + public void Setup() + { + _world = World.Create(); + } + + public unsafe void Run() + { + using var scope = AllocationManager.CreateStackScope(); + var set1 = new ComponentSet(scope.AllocationHandle, ComponentTypeID.Value); + var set2 = new ComponentSet(scope.AllocationHandle, ComponentTypeID.Value, ComponentTypeID.Value); + + var e1 = _world.EntityManager.CreateEntity(set1); + var e2 = _world.EntityManager.CreateEntity(set2); + + _world.EntityManager.SetComponent(e1, new Transform { position = new float3(1, 2, 3) }); + _world.EntityManager.SetComponent(e2, new Transform { position = new float3(4, 5, 6) }); + _world.EntityManager.SetComponent(e2, new Mesh { index = 42 }); + + using var stream = new MemoryStream(); + var serializeOptions = new JsonSerializerOptions + { + IncludeFields = true, + IgnoreReadOnlyProperties = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { typeInfo => + { + // Remove everything from the serialization list that is not a field + foreach (var property in typeInfo.Properties) + { + if (property.AttributeProvider is not System.Reflection.FieldInfo) + { + property.ShouldSerialize = (_, _) => false; + } + } + } + } + } + }; + + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + + writer.WriteStartObject(); + writer.WriteString("Name", "world 1"); + writer.WriteStartArray("Entities"); + + for (var i = 0; i < _world.ArchetypeCount; i++) + { + ref var archetype = ref _world.GetArchetypeReference(i); + + for (var j = 0; j < archetype.ChunkCount; j++) + { + ref var chunk = ref archetype.GetChunkReference(j); + for (var k = 0; k < chunk._count; k++) + { + writer.WriteStartObject(); + + var entity = *(Entity*)(chunk.GetUnsafePtr() + archetype.EntityIDsOffset + k * sizeof(Entity)); + writer.WriteNumber("ID", entity.ID); + writer.WriteStartArray("Components"); + + foreach (var layout in archetype._layouts) + { + var type = ComponentRegistry.s_runtimeIDToType[layout.componentID]; + var size = ComponentRegistry.GetComponentInfo(layout.componentID).size; + + if (type.AssemblyQualifiedName == null) + { + continue; + } + + writer.WriteStartObject(); + writer.WriteString("Type", type.AssemblyQualifiedName); + writer.WritePropertyName("Data"); + + var pComponentData = chunk.GetUnsafePtr() + layout.offset + (k * size); + var instace = Marshal.PtrToStructure((nint)pComponentData, type); + JsonSerializer.Serialize(writer, instace, type, serializeOptions); + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + } + } + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + + writer.Flush(); + + var data = stream.ToArray(); + + var json = System.Text.Encoding.UTF8.GetString(data); + Console.WriteLine(json); + + + var reader = new Utf8JsonReader(data); + + var root = JsonDocument.ParseValue(ref reader).RootElement; + var name = root.GetProperty("Name").GetString(); + Console.WriteLine($"Deserialized World Name: {name}"); + + var entityData = new List<(int EntityID, Type ComponentType, object Instance)>(); + + foreach (var entityElement in root.GetProperty("Entities").EnumerateArray()) + { + var id = entityElement.GetProperty("ID").GetInt32(); + + // Access the new "Components" array + var componentsElement = entityElement.GetProperty("Components"); + + foreach (var componentElement in componentsElement.EnumerateArray()) + { + var typeName = componentElement.GetProperty("Type").GetString(); + var dataElement = componentElement.GetProperty("Data"); + + var type = Type.GetType(typeName!); + if (type == null) + { + continue; + } + + var instance = dataElement.Deserialize(type, serializeOptions); + if (instance != null) + { + entityData.Add((id, type, instance)); + } + } + } + + foreach (var (id, type, instance) in entityData) + { + Console.WriteLine($"Entity ID: {id}, Component: {type.Name}, Data: {instance}"); + } + } + + public void Cleanup() + { + _world.Dispose(); + } +} diff --git a/Ghost.Entities.Test/SystemTest.cs b/Ghost.Entities.Test/SystemTest.cs index fe426fa..4902143 100644 --- a/Ghost.Entities.Test/SystemTest.cs +++ b/Ghost.Entities.Test/SystemTest.cs @@ -1,17 +1,14 @@ using Ghost.Test.Core; -using Misaki.HighPerformance.Jobs; namespace Ghost.Entities.Test; internal class SystemTest : ITest { - private JobScheduler _jobScheduler = null!; private World _world = null!; public void Setup() { - _jobScheduler = new JobScheduler(4); - _world = World.Create(_jobScheduler); + _world = World.Create(); } public void Run() @@ -29,8 +26,6 @@ internal class SystemTest : ITest public void Cleanup() { _world.Dispose(); - _jobScheduler.Dispose(); - JobScheduler.ReleaseTempAllocator(); } } diff --git a/Ghost.Entities/Archetype.cs b/Ghost.Entities/Archetype.cs index fe8fb76..ff27b17 100644 --- a/Ghost.Entities/Archetype.cs +++ b/Ghost.Entities/Archetype.cs @@ -371,6 +371,16 @@ internal unsafe struct Archetype : IDisposable _chunks.Add(newChunk); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Entity GetEntity(int chunkIndex, int rowIndex) + { + var chunk = _chunks[chunkIndex]; + var chunkBase = chunk.GetUnsafePtr(); + var src = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex); + + return *(Entity*)src; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void SetEntity(int chunkIndex, int rowIndex, Entity entity) { diff --git a/Ghost.Entities/EntityManager.cs b/Ghost.Entities/EntityManager.cs index d392536..d078ed6 100644 --- a/Ghost.Entities/EntityManager.cs +++ b/Ghost.Entities/EntityManager.cs @@ -161,16 +161,17 @@ public unsafe partial class EntityManager : IDisposable /// /// Create multiple entities with specified components. /// - /// The allocator to use for the returned array. + /// The span to store the created entities. /// A set of component type IDs to add to the entities. /// An array of the created entities. public void CreateEntities(Span entities, ComponentSet set) { - var arcID = _world.GetArchetypeIDBySignatureHash(set.GetHashCode()); + var hash = set.GetHashCode(); + var arcID = _world.GetArchetypeIDBySignatureHash(hash); if (arcID.IsInvalid) { - arcID = _world.CreateArchetype(set.Components, set.GetHashCode()); + arcID = _world.CreateArchetype(set.Components, hash); } ref var archetype = ref _world.GetArchetypeReference(arcID); diff --git a/Ghost.Entities/EntityQuery.JobChunk.cs b/Ghost.Entities/EntityQuery.JobChunk.cs index a1ea698..0248a30 100644 --- a/Ghost.Entities/EntityQuery.JobChunk.cs +++ b/Ghost.Entities/EntityQuery.JobChunk.cs @@ -48,6 +48,10 @@ public unsafe partial struct EntityQuery where TJob : unmanaged, IJobChunk { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } var chunkInfos = new UnsafeList(_matchingArchetypes.Count * 2, JobScheduler.TempAllocatorHandle); diff --git a/Ghost.Entities/Query.cs b/Ghost.Entities/Query.cs index 7eed2df..54560a1 100644 --- a/Ghost.Entities/Query.cs +++ b/Ghost.Entities/Query.cs @@ -597,7 +597,7 @@ public ref partial struct QueryBuilder return queryID; } - private void Dispose() + private readonly void Dispose() { _scope.Dispose(); } diff --git a/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs b/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs index eaf05f3..62b1dcd 100644 --- a/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs +++ b/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs @@ -1096,6 +1096,10 @@ public unsafe partial struct EntityQuery where T0 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); @@ -1229,6 +1233,10 @@ public unsafe partial struct EntityQuery where T1 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); @@ -1389,6 +1397,10 @@ public unsafe partial struct EntityQuery where T2 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); @@ -1576,6 +1588,10 @@ public unsafe partial struct EntityQuery where T3 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); @@ -1790,6 +1806,10 @@ public unsafe partial struct EntityQuery where T4 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); @@ -2031,6 +2051,10 @@ public unsafe partial struct EntityQuery where T5 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); @@ -2299,6 +2323,10 @@ public unsafe partial struct EntityQuery where T6 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); @@ -2594,6 +2622,10 @@ public unsafe partial struct EntityQuery where T7 : unmanaged, IComponent { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); diff --git a/Ghost.Entities/Templates/EntityQuery.JobEntity.tt b/Ghost.Entities/Templates/EntityQuery.JobEntity.tt index ddebccc..fd020e7 100644 --- a/Ghost.Entities/Templates/EntityQuery.JobEntity.tt +++ b/Ghost.Entities/Templates/EntityQuery.JobEntity.tt @@ -126,6 +126,10 @@ public unsafe partial struct EntityQuery <#= restrictions #> { var world = World.GetWorld(_worldID).GetValueOrThrow(); + if (world.JobScheduler == null) + { + throw new InvalidOperationException("The World has no JobScheduler assigned."); + } // 1. Flatten the World var chunks = new UnsafeList(128, JobScheduler.TempAllocatorHandle); diff --git a/Ghost.Entities/World.cs b/Ghost.Entities/World.cs index ee39525..0086d15 100644 --- a/Ghost.Entities/World.cs +++ b/Ghost.Entities/World.cs @@ -11,11 +11,11 @@ public partial class World private static readonly List s_worlds = new(4); private static readonly Queue> s_freeWorldSlots = new(); - internal static Identifier EmptyArchetypeID => new (0); + internal static Identifier EmptyArchetypeID => new(0); public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count; - public static World Create(JobScheduler jobScheduler, int entityCapacity = 16) + public static World Create(JobScheduler? jobScheduler = null, int entityCapacity = 16) { lock (s_worlds) { @@ -79,11 +79,11 @@ public partial class World public partial class World : IDisposable, IEquatable { private readonly Identifier _id; - private readonly JobScheduler _jobScheduler; + private readonly JobScheduler? _jobScheduler; private readonly EntityManager _entityManager; private readonly EntityCommandBuffer _entityCommandBuffer; - private readonly EntityCommandBuffer[] _threadLocalECBs; + private readonly EntityCommandBuffer[]? _threadLocalECBs; private readonly SystemManager _systemManager; @@ -106,7 +106,7 @@ public partial class World : IDisposable, IEquatable /// /// Gets the job scheduler associated with this world. /// - public JobScheduler JobScheduler => _jobScheduler; + public JobScheduler? JobScheduler => _jobScheduler; /// /// Gets the publicntity manager for this world. @@ -131,14 +131,13 @@ public partial class World : IDisposable, IEquatable /// public EntityCommandBuffer EntityCommandBuffer => _entityCommandBuffer; - private World(Identifier id, int entityCapacity, JobScheduler jobScheduler) + private World(Identifier id, int entityCapacity, JobScheduler? jobScheduler) { _id = id; _jobScheduler = jobScheduler; _entityManager = new EntityManager(this, entityCapacity); _entityCommandBuffer = new EntityCommandBuffer(_entityManager); - _threadLocalECBs = new EntityCommandBuffer[jobScheduler.WorkerCount]; _systemManager = new SystemManager(this); @@ -148,9 +147,13 @@ public partial class World : IDisposable, IEquatable _archetypeLookup = new UnsafeHashMap>(16, Allocator.Persistent); _querieLookup = new UnsafeHashMap>(16, Allocator.Persistent); - for (var i = 0; i < jobScheduler.WorkerCount; i++) + if (jobScheduler != null) { - _threadLocalECBs[i] = new EntityCommandBuffer(_entityManager); + _threadLocalECBs = new EntityCommandBuffer[jobScheduler.WorkerCount]; + for (var i = 0; i < jobScheduler.WorkerCount; i++) + { + _threadLocalECBs[i] = new EntityCommandBuffer(_entityManager); + } } // Create the empty archetype @@ -227,9 +230,12 @@ public partial class World : IDisposable, IEquatable { _entityCommandBuffer.Playback(); - for (var i = 0; i < _threadLocalECBs.Length; i++) + if (_threadLocalECBs != null) { - _threadLocalECBs[i].Playback(); + for (var i = 0; i < _threadLocalECBs.Length; i++) + { + _threadLocalECBs[i].Playback(); + } } } @@ -254,6 +260,11 @@ public partial class World : IDisposable, IEquatable [MethodImpl(MethodImplOptions.AggressiveInlining)] public EntityCommandBuffer GetThreadLocalEntityCommandBuffer(int threadIndex) { + if (_threadLocalECBs == null) + { + throw new InvalidOperationException("This world does not have a JobScheduler associated with it."); + } + return _threadLocalECBs[threadIndex]; } @@ -301,9 +312,13 @@ public partial class World : IDisposable, IEquatable _entityManager.Dispose(); _entityCommandBuffer.Dispose(); - foreach (var v in _threadLocalECBs) + + if (_threadLocalECBs != null) { - v.Dispose(); + foreach (var v in _threadLocalECBs) + { + v.Dispose(); + } } _archetypes.Dispose();