Remove old SceneGraph

This commit is contained in:
2026-01-25 21:18:16 +09:00
parent 0201f0fc33
commit ba5dc2159e
17 changed files with 120 additions and 1427 deletions

View File

@@ -1,72 +0,0 @@
using Ghost.Editor.Core.Progress;
using Ghost.Editor.Core.Resources;
using Ghost.Editor.Core.Serializer;
using Ghost.Editor.Core.Utilities;
using Ghost.Engine.Core;
using System.Text.Json;
namespace Ghost.Editor.Core.SceneGraph;
/// <summary>
/// Editor-specific scene manager that uses JSON serialization for human-readable scene files.
/// </summary>
/// <remarks>
/// This manager provides JSON-based serialization suitable for editor workflows.
/// Runtime applications should use SceneManager with binary serialization.
/// </remarks>
public static class EditorSceneManager
{
// TODO: Use guid keys instead of string paths for better performance and uniqueness
private static readonly Dictionary<string, SceneNode> s_loadedWorlds = new();
public static IEnumerable<SceneNode> LoadedWorlds => s_loadedWorlds.Values;
public static event Action<SceneNode>? OnWorldLoaded;
public static event Action<SceneNode>? OnWorldUnloaded;
/// <summary>
/// Loads a scene from a JSON file.
/// </summary>
/// <param name="worldPath">The path to the JSON scene file.</param>
public static async Task LoadSceneAsync(string worldPath)
{
if (s_loadedWorlds.ContainsKey(worldPath)
|| !File.Exists(worldPath)
|| Path.GetExtension(worldPath) != FileExtensions.SCENE_FILE_EXTENSION)
{
return;
}
var progressService = EditorApplication.GetService<IProgressService>();
progressService.ShowIndeterminateProgress("Loading world...");
// Unload existing scenes
foreach (var world in s_loadedWorlds)
{
world.Value.Unload();
OnWorldUnloaded?.Invoke(world.Value);
}
s_loadedWorlds.Clear();
// TODO: Get or create World instance for editor
// For now, keep compatibility with old SceneNode deserialization
await using var readStream = new FileStream(worldPath, FileMode.Open, FileAccess.Read, FileShare.Read);
var deserializedScene = await JsonSerializer.DeserializeAsync<SceneNode>(readStream, Engine.Resources.EngineResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed.");
s_loadedWorlds[worldPath] = deserializedScene;
await deserializedScene.LoadAsync();
progressService.HideProgress();
OnWorldLoaded?.Invoke(deserializedScene);
}
/// <summary>
/// Saves a scene to a JSON file using the new serializer.
/// </summary>
/// <param name="scene">The scene to save.</param>
/// <param name="filePath">The path to save the JSON scene file.</param>
public static async Task SaveSceneAsync(Scene scene, string filePath)
{
await SceneSerializer.SaveSceneAsync(scene.World, scene.ID, filePath, scene.Name);
}
}

View File

@@ -1,124 +0,0 @@
using Ghost.Editor.Core.Controls.Internal;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Resources;
using Ghost.Engine.Editor;
using Ghost.Entities;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using System.Reflection;
namespace Ghost.Editor.Core.SceneGraph;
public partial class EntityNode : SceneGraphNode
{
public SceneNode Owner
{
get;
set;
}
public Entity Entity
{
get;
}
public override SceneGraphNodeType NodeType => SceneGraphNodeType.Entity;
public EntityNode(SceneNode owner, Entity entity, string name)
{
Owner = owner;
Entity = entity;
Name = name;
}
}
public partial class EntityNode : IInspectable
{
public IconSource? Icon => EditorIconSource.entity_24;
public UIElement? HeaderContent
{
get
{
var root = new StackPanel()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center
};
var nameText = new TextBox
{
Text = Name,
FontWeight = FontWeights.Bold,
};
var idText = new TextBlock
{
Text = $"ID: {Entity.ID} Generation: {Entity.Generation}",
Margin = new Thickness(5, 7, 0, 0),
Opacity = 0.75,
Style = Application.Current.Resources["CaptionTextBlockStyle"] as Style
};
nameText.SetBinding(TextBox.TextProperty, new Binding
{
Source = this,
Path = new PropertyPath(nameof(Name)),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
});
root.Children.Add(nameText);
root.Children.Add(idText);
return root;
}
}
public unsafe UIElement? InspectorContent
{
get
{
var r = Owner.Scene.World.EntityManager.GetEntityLocation(Entity);
if (!r)
{
return null;
}
var root = new StackPanel()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Top
};
var location = r.Value;
ref var archetype = ref Owner.Scene.World.ComponentManager.GetArchetypeReference(location.archetypeID);
var it = archetype._signature.GetIterator();
while (it.Next(out var typeID))
{
var pComponent = archetype.GetComponentData(location.chunkIndex, location.rowIndex, typeID);
if (pComponent == null)
{
continue;
}
if (!ComponentRegistry.s_runtimeIDToType.TryGetValue(typeID, out var t))
{
continue;
}
if (t.GetCustomAttribute<HideEditorAttribute>() != null)
{
continue;
}
var componentView = new ComponentView(t.Name, Owner.Scene.World, Entity, t);
root.Children.Add(componentView);
}
return root;
}
}
}

View File

@@ -0,0 +1,68 @@
# Architecture Plan: Scene Graph and Scene Representation
The Scene Graph is a hierarchical structure that represents all the objects and entities within a 3D scene in the Ghost Editor.
## Scene Graph (Editor representation of runtime data)
There should be two main types of nodes in the Scene Graph:
1. **Entity Node**: Represents an individual entity within a scene.
2. **Scene Node**: Represents a Scene object, which can contain multiple entities.
### Editor World
Editor contains a different world compares to the runtime world. When user click the Play button, we will create a runtime world and load the scene data from the editor world to the runtime world.
This allows us to
1. Unload the runtime only systems like physics, rendering, etc when user stop playing.
2. Load editor only systems like gizmos, debug, etc when user stop playing.
3. Allow editor only entities like editor camera, editor lights, etc to exist in the editor world without affecting the runtime world.
### Editor Hierarchy
The Scene Graph should be represented as a tree structure in the editor (TreeView in WinUI 3), where:
- The top level nodes represents the loaded Scenes in the editor world.
- Levels below the Scene nodes represents the Entity nodes that belong to that scene.
- Each Entity node can have child Entity nodes representing parent-child relationships between entities.
An example hierarchy could look like this:
```
- Scene 1
- Entity A
- Entity B
- Entity C
- Scene 2
- Entity D
```
## Scene (The runtime representation)
A Scene is a collection of entities with SceneID component from a world that are grouped together. There can be multiple scenes in a world.
### Save a Scene
When save a scene, all entities with the SceneID component matching the scene's ID should be included in the saved data.
When an Entity references another Entity in the same scene, we should store the file local id instead of the global entity id.
For example, if Entity A (id: 10, 5th in scene) references Entity B (id: 20, 50th in scene) in the same scene, in the saved data for Entity A,
we should store 50 (the file local id) as the reference to Entity B instead of 20 (the global entity id).
> We does not allow cross-scene references for now because ideally it's not a good practice to have cross-scene references.
We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
### Load a Scene
When loading a scene, we need to reconstruct the entities and their relationships based on the saved data.
1. We allocate the entities in the world and assign them new global entity IDs.
2. We remap the file local IDs to the new global entity IDs and change the references accordingly.
For example if Entity A (file local id: 5) references Entity B (file local id: 50) in the saved data,
we need to find the new global entity IDs for both entities after loading and update the reference in Entity A to point to the new global entity ID of Entity B.
### Data format
The scene data should be stored in a structured format (e.g., JSON or binary) that includes:
- Scene metadata (e.g., name, ID)
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
- References between entities using file local IDs
JSON should only be used in the editor and JSON serialization/deserialization logic should also only exist in the editor codebase (Ghost.Editor.Core). Reflection is allowed here.
Binary format should be used in the runtime for better performance. The runtime codebase (Ghost.Engine) must be aot compatible.
Currently we strict the IComponent to must be unmanaged and blittable types.
However, we also support ManagedEntity and ManagedEntityRef with ScriptComponent to allow OOP like logic for common gameplay logic that DOD pattern is not suitable for.
Serializing/deserializing with those components will be tricky. We can use MemoryPack for binary serialization/deserialization because it supports both unmanaged and managed types.

View File

@@ -1,112 +0,0 @@
using Ghost.Engine.Components;
using Ghost.Entities;
namespace Ghost.Editor.Core.SceneGraph;
public class SceneGraphHelpers
{
/// <summary>
/// Creates a new <see cref="EntityNode"/> entity with default components.
/// </summary>
/// <param name="world">The world context where the entity will be created.</param>
/// <param name="entity">The entity to be wrapped in the <see cref="EntityNode"/>.</param>
public static EntityNode CreateEntityNode(SceneNode owner, Entity entity, string name)
{
owner.Scene.World.EntityManager.AddComponent(entity, new LocalToWorld { matrix = Misaki.HighPerformance.Mathematics.float4x4.identity });
owner.Scene.World.EntityManager.AddComponent(entity, Hierarchy.Root);
return new EntityNode(owner, entity, name);
}
/// <summary>
/// Creates a new <see cref="Entity"/> and <see cref="EntityNode"/> entity with default components.
/// </summary>
/// <param name="owner">The world context where the entity will be created.</param>
public static EntityNode CreateEntityNode(SceneNode owner, string name)
{
var entity = owner.Scene.World.EntityManager.CreateEntity();
return CreateEntityNode(owner, entity, name);
}
/// <summary>
/// Attaches childEntity to parentEntity in the scene graph.
/// </summary>
/// <param name="world">The world context where the entities exist.</param>
/// <param name="parentNode">The parent entity to which the child will be attached.</param>
/// <param name="childNode">The child entity to be attached.</param>
public static void AttachChild(SceneNode scene, EntityNode parentNode, EntityNode childNode)
{
// 1) If the child already has a parent, detach it first
var childHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(childNode.Entity);
if (childHierarchy.parent != Entity.Invalid)
{
DetachFromParent(scene, childNode);
}
// 2) Link child to new parent
childHierarchy.parent = parentNode.Entity;
// 3) Insert child at the head of parent's child list
var parentHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(parentNode.Entity);
childHierarchy.nextSibling = parentHierarchy.firstChild;
parentHierarchy.firstChild = childNode.Entity;
// 4) Write back
scene.Scene.World.EntityManager.SetComponent(parentNode.Entity, parentHierarchy);
scene.Scene.World.EntityManager.SetComponent(childNode.Entity, childHierarchy);
// 5) Update children list in parent node
parentNode.AddChild(childNode);
}
/// <summary>
/// Detaches the specified entity from its parent in the scene graph.
/// </summary>
/// <param name="world">The world context where the entities exist.</param>
/// <param name="node">The entity to detach from its parent.</param>
public static void DetachFromParent(SceneNode scene, EntityNode node)
{
var hierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(node.Entity);
var parent = hierarchy.parent;
if (parent == Entity.Invalid)
{
return; // already root
}
var parentHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(parent);
// If entity is the first child, simply move head
if (parentHierarchy.firstChild == node.Entity)
{
parentHierarchy.firstChild = hierarchy.nextSibling;
}
else
{
// Otherwise, find the previous sibling in the linked list
var prevSibling = parentHierarchy.firstChild;
while (prevSibling != Entity.Invalid)
{
var prevHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(prevSibling);
if (prevHierarchy.nextSibling == node.Entity)
{
prevHierarchy.nextSibling = hierarchy.nextSibling;
scene.Scene.World.EntityManager.SetComponent(prevSibling, prevHierarchy);
break;
}
prevSibling = prevHierarchy.nextSibling;
}
}
// Clear child's references
hierarchy.parent = Entity.Invalid;
hierarchy.nextSibling = Entity.Invalid;
// Write back
scene.Scene.World.EntityManager.SetComponent(parent, parentHierarchy);
scene.Scene.World.EntityManager.SetComponent(node.Entity, hierarchy);
// Remove from parent's children list
scene.EntityNodeLookup[parent].RemoveChild(node);
}
}

View File

@@ -1,54 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
public enum SceneGraphNodeType
{
Scene,
Entity,
}
public abstract partial class SceneGraphNode : ObservableObject
{
public ObservableCollection<SceneGraphNode>? Children
{
get;
private set;
}
[ObservableProperty]
public partial string Name
{
get;
set;
}
public abstract SceneGraphNodeType NodeType
{
get;
}
public int ChildCount => Children?.Count ?? 0;
public virtual void AddChild(SceneGraphNode child)
{
Children ??= new();
Children.Add(child);
}
public virtual bool RemoveChild(SceneGraphNode child)
{
return Children?.Remove(child) ?? false;
}
public SceneGraphNode GetChild(int index)
{
if (Children == null || index < 0 || index >= Children.Count)
{
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
}
return Children[index];
}
}

View File

@@ -1,180 +0,0 @@
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Resources;
using Ghost.Engine.Components;
using Ghost.Engine.Core;
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.SceneGraph;
public partial class SceneNode : SceneGraphNode, IEquatable<SceneNode>
{
private Scene _scene;
private Dictionary<Entity, EntityNode> _entityNodeLookup = new();
public Scene Scene => _scene;
public Dictionary<Entity, EntityNode> EntityNodeLookup => _entityNodeLookup;
public override SceneGraphNodeType NodeType => SceneGraphNodeType.Scene;
public SceneNode(Scene scene, string name)
{
_scene = scene;
Name = name;
}
private void UpdateLookup(Entity key, EntityNode value)
{
_entityNodeLookup[key] = value;
if (value.Children == null)
{
return;
}
foreach (var child in value.Children)
{
if (child is EntityNode entityChild)
{
UpdateLookup(entityChild.Entity, entityChild);
}
}
}
public override void AddChild(SceneGraphNode child)
{
if (child is not EntityNode entityNode)
{
throw new ArgumentException("Child must be of type EntityNode.", nameof(child));
}
base.AddChild(entityNode);
UpdateLookup(entityNode.Entity, entityNode);
}
public override bool RemoveChild(SceneGraphNode child)
{
if (child is not EntityNode entityNode)
{
throw new ArgumentException("Child must be of type EntityNode.", nameof(child));
}
var result = base.RemoveChild(child);
if (result)
{
_entityNodeLookup.Remove(entityNode.Entity);
}
return result;
}
private EntityNode BuildNodeRecursive(Entity entity)
{
if (!_entityNodeLookup.TryGetValue(entity, out var node))
{
node = new EntityNode(this, entity, "New Entity");
_entityNodeLookup[entity] = node;
}
var hc = _scene.World.EntityManager.GetComponent<Hierarchy>(entity);
var child = hc.firstChild;
while (child != Entity.Invalid)
{
node.AddChild(BuildNodeRecursive(child));
var childHC = _scene.World.EntityManager.GetComponent<Hierarchy>(child);
child = childHC.nextSibling;
}
return node;
}
private void BuildGraph()
{
var queryID = new QueryBuilder()
.WithAll<Hierarchy>()
.Build(_scene.World);
_scene.World.ComponentManager.GetEntityQueryReference(queryID).ForEach<Hierarchy>((entity, ref hierarchy) =>
{
if (hierarchy.parent == Entity.Invalid)
{
var node = BuildNodeRecursive(entity);
AddChild(node);
}
});
}
public Task LoadAsync()
{
return Task.Run(BuildGraph);
}
public void Unload()
{
_scene = null!;
Children?.Clear();
_entityNodeLookup.Clear();
}
public override string ToString()
{
return $"WorldNode: {Name} (World ID: {_scene.ID})";
}
public override int GetHashCode()
{
return HashCode.Combine(_scene, Name);
}
public override bool Equals(object? obj)
{
return obj is SceneNode other && Equals(other);
}
public bool Equals(SceneNode? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return _scene.Equals(other._scene) && Name == other.Name;
}
public static bool operator ==(SceneNode? left, SceneNode? right)
{
if (left is null)
{
return right is null;
}
return left.Equals(right);
}
public static bool operator !=(SceneNode? left, SceneNode? right)
{
return !(left == right);
}
}
public partial class SceneNode : IInspectable
{
public IconSource? Icon => EditorIconSource.scene_24;
[AssetOpenHandler(FileExtensions.SCENE_FILE_EXTENSION)]
public static async void Open(string path)
{
await EditorSceneManager.LoadSceneAsync(path);
}
public UIElement? HeaderContent => null;
public UIElement? InspectorContent => null;
}