ECS refactor: new ComponentSet, serialization, generators

Major ECS API overhaul: added ComponentSet, refactored ComponentRegistry, and updated all entity/component creation methods. Introduced robust custom serialization infrastructure and per-component source generators for registration and (de)serialization. Updated editor, engine, and test code to use new APIs. Improved code quality, naming, and performance throughout. Removed obsolete code and updated dependencies.
This commit is contained in:
2025-12-20 20:41:40 +09:00
parent 3118021272
commit 00b4e82ded
60 changed files with 1216 additions and 814 deletions

View File

@@ -15,15 +15,15 @@ public enum OpenWorldMode
public static class EditorWorldManager
{
// TODO: Use guid keys instead of string paths for better performance and uniqueness
private static readonly Dictionary<string, WorldNode> _loadedWorlds = new();
public static IEnumerable<WorldNode> LoadedWorlds => _loadedWorlds.Values;
private static readonly Dictionary<string, WorldNode> s_loadedWorlds = new();
public static IEnumerable<WorldNode> LoadedWorlds => s_loadedWorlds.Values;
public static event Action<WorldNode>? OnWorldLoaded;
public static event Action<WorldNode>? OnWorldUnloaded;
public static async Task LoadWorld(string worldPath)
{
if (_loadedWorlds.ContainsKey(worldPath)
if (s_loadedWorlds.ContainsKey(worldPath)
|| !File.Exists(worldPath)
|| Path.GetExtension(worldPath) != FileExtensions.SCENE_FILE_EXTENSION)
{
@@ -33,18 +33,18 @@ public static class EditorWorldManager
var progressService = EditorApplication.GetService<IProgressService>();
progressService.ShowIndeterminateProgress("Loading world...");
foreach (var world in _loadedWorlds)
foreach (var world in s_loadedWorlds)
{
world.Value.Unload();
OnWorldUnloaded?.Invoke(world.Value);
}
await using var readStream = new FileStream(worldPath, FileMode.Open, FileAccess.Read, FileShare.Read);
var deserializedScene = await JsonSerializer.DeserializeAsync<WorldNode>(readStream, Engine.Resources.StaticResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed.");
var deserializedScene = await JsonSerializer.DeserializeAsync<WorldNode>(readStream, Engine.Resources.EngineResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed.");
_loadedWorlds.Clear();
s_loadedWorlds.Clear();
_loadedWorlds[worldPath] = deserializedScene;
s_loadedWorlds[worldPath] = deserializedScene;
await deserializedScene.LoadAsync();
progressService.HideProgress();

View File

@@ -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.SparseEntities;
using Ghost.Entities;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -76,31 +76,46 @@ public partial class EntityNode : IInspectable
}
}
public UIElement? InspectorContent
public unsafe UIElement? InspectorContent
{
get
{
var r = Owner.World.EntityManager.GetEntityLocation(Entity);
if (!r)
{
return null;
}
var root = new StackPanel()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Top
};
foreach (var (typeHandle, componentPtr) in Owner.World.EntityManager.GetComponentsUnsafe(Entity))
var location = r.Value;
ref var archetype = ref Owner.World.GetArchetypeReference(location.archetypeID);
var it = archetype._signature.GetIterator();
while (it.Next(out var typeID))
{
if (componentPtr == IntPtr.Zero)
var pComponent = archetype.GetComponentData(location.chunkIndex, location.rowIndex, typeID);
if (pComponent == null)
{
continue;
}
var type = typeHandle.ToType();
if (type == null || type.GetCustomAttribute<HideEditorAttribute>() != null)
if (!ComponentRegistry.s_runtimeIDToType.TryGetValue(typeID, out var t))
{
continue;
}
var dataView = new ComponentDataView(type.Name, Owner.World, Entity, type);
root.Children.Add(dataView);
if (t.GetCustomAttribute<HideEditorAttribute>() != null)
{
continue;
}
var componentView = new ComponentView(t.Name, Owner.World, Entity, t);
root.Children.Add(componentView);
}
return root;

View File

@@ -1,5 +1,5 @@
using Ghost.Engine.Components;
using Ghost.SparseEntities;
using Ghost.Entities;
namespace Ghost.Editor.Core.SceneGraph;
@@ -37,23 +37,23 @@ public class SceneGraphHelpers
{
// 1) If the child already has a parent, detach it first
var childHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(childNode.Entity);
if (childHierarchy.ValueRO.parent != Entity.Invalid)
if (childHierarchy.parent != Entity.Invalid)
{
DetachFromParent(scene, childNode);
}
// 2) Link child to new parent
childHierarchy.ValueRW.parent = parentNode.Entity;
childHierarchy.parent = parentNode.Entity;
// 3) Insert child at the head of parent's child list
var parentHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(parentNode.Entity);
childHierarchy.ValueRW.nextSibling = parentHierarchy.ValueRO.firstChild;
parentHierarchy.ValueRW.firstChild = childNode.Entity;
childHierarchy.nextSibling = parentHierarchy.firstChild;
parentHierarchy.firstChild = childNode.Entity;
// 4) Write back
scene.World.EntityManager.SetComponent(parentNode.Entity, in parentHierarchy.ValueRO);
scene.World.EntityManager.SetComponent(childNode.Entity, in childHierarchy.ValueRO);
scene.World.EntityManager.SetComponent(parentNode.Entity, parentHierarchy);
scene.World.EntityManager.SetComponent(childNode.Entity, childHierarchy);
// 5) Update children list in parent node
parentNode.AddChild(childNode);
@@ -67,7 +67,7 @@ public class SceneGraphHelpers
public static void DetachFromParent(WorldNode scene, EntityNode node)
{
var hierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(node.Entity);
var parent = hierarchy.ValueRO.parent;
var parent = hierarchy.parent;
if (parent == Entity.Invalid)
{
return; // already root
@@ -76,35 +76,35 @@ public class SceneGraphHelpers
var parentHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(parent);
// If entity is the first child, simply move head
if (parentHierarchy.ValueRO.firstChild == node.Entity)
if (parentHierarchy.firstChild == node.Entity)
{
parentHierarchy.ValueRW.firstChild = hierarchy.ValueRO.nextSibling;
parentHierarchy.firstChild = hierarchy.nextSibling;
}
else
{
// Otherwise, find the previous sibling in the linked list
var prevSibling = parentHierarchy.ValueRO.firstChild;
var prevSibling = parentHierarchy.firstChild;
while (prevSibling != Entity.Invalid)
{
var prevHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(prevSibling);
if (prevHierarchy.ValueRW.nextSibling == node.Entity)
if (prevHierarchy.nextSibling == node.Entity)
{
prevHierarchy.ValueRW.nextSibling = hierarchy.ValueRO.nextSibling;
scene.World.EntityManager.SetComponent(prevSibling, in prevHierarchy.ValueRO);
prevHierarchy.nextSibling = hierarchy.nextSibling;
scene.World.EntityManager.SetComponent(prevSibling, prevHierarchy);
break;
}
prevSibling = prevHierarchy.ValueRO.nextSibling;
prevSibling = prevHierarchy.nextSibling;
}
}
// Clear child's references
hierarchy.ValueRW.parent = Entity.Invalid;
hierarchy.ValueRW.nextSibling = Entity.Invalid;
hierarchy.parent = Entity.Invalid;
hierarchy.nextSibling = Entity.Invalid;
// Write back
scene.World.EntityManager.SetComponent(parent, in parentHierarchy.ValueRO);
scene.World.EntityManager.SetComponent(node.Entity, in hierarchy.ValueRO);
scene.World.EntityManager.SetComponent(parent, parentHierarchy);
scene.World.EntityManager.SetComponent(node.Entity, hierarchy);
// Remove from parent's children list
scene.EntityNodeLookup[parent].RemoveChild(node);

View File

@@ -3,14 +3,14 @@ using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Resources;
using Ghost.Editor.Core.Serializer;
using Ghost.Engine.Components;
using Ghost.SparseEntities;
using Ghost.Engine.IO;
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Text.Json.Serialization;
namespace Ghost.Editor.Core.SceneGraph;
[JsonConverter(typeof(WorldNodeSerializer))]
[CustomSerializer(typeof(WorldNodeSerializer))]
public partial class WorldNode : SceneGraphNode, IEquatable<WorldNode>
{
private World _world;
@@ -27,11 +27,6 @@ public partial class WorldNode : SceneGraphNode, IEquatable<WorldNode>
Name = name;
}
internal WorldNode()
{
_world = World.Create();
}
private void UpdateLookup(Entity key, EntityNode value)
{
_entityNodeLookup[key] = value;
@@ -85,13 +80,13 @@ public partial class WorldNode : SceneGraphNode, IEquatable<WorldNode>
}
var hc = _world.EntityManager.GetComponent<Hierarchy>(entity);
var child = hc.ValueRO.firstChild;
var child = hc.firstChild;
while (child != Entity.Invalid)
{
node.AddChild(BuildNodeRecursive(child));
var childHC = _world.EntityManager.GetComponent<Hierarchy>(child);
child = childHC.ValueRO.nextSibling;
child = childHC.nextSibling;
}
return node;
@@ -99,14 +94,18 @@ public partial class WorldNode : SceneGraphNode, IEquatable<WorldNode>
private void BuildGraph()
{
foreach (var (entity, hierarchy) in _world.Query<Hierarchy>())
var queryID = new QueryBuilder()
.WithAll<Hierarchy>()
.Build(_world);
_world.GetEntityQueryReference(queryID).ForEach<Hierarchy>((entity, ref hierarchy) =>
{
if (hierarchy.ValueRO.parent == Entity.Invalid)
if (hierarchy.parent == Entity.Invalid)
{
var node = BuildNodeRecursive(entity);
AddChild(node);
}
}
});
}
public Task LoadAsync()
@@ -116,7 +115,6 @@ public partial class WorldNode : SceneGraphNode, IEquatable<WorldNode>
public void Unload()
{
_world.Dispose();
_world = null!;
Children?.Clear();