Update scene graph

This commit is contained in:
2026-01-26 13:59:33 +09:00
parent 06a150b899
commit 8df0b46960
18 changed files with 144 additions and 1602 deletions

View File

@@ -1,285 +0,0 @@
using Ghost.Engine.Components;
using Ghost.Engine.Core;
using Ghost.Entities;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
/// <summary>
/// Manages the editor world and scene graph hierarchy.
/// Provides functionality to load/unload scenes and maintain the editor-side scene graph.
/// </summary>
public class EditorWorldManager
{
private readonly World _editorWorld;
private readonly SceneManager _sceneManager;
private readonly ObservableCollection<SceneNode> _loadedScenes;
private readonly Dictionary<short, SceneNode> _sceneIdToNode;
private readonly Dictionary<Entity, EntityNode> _entityToNode;
/// <summary>
/// Gets the editor world instance.
/// </summary>
public World EditorWorld => _editorWorld;
/// <summary>
/// Gets the runtime scene manager.
/// </summary>
public SceneManager SceneManager => _sceneManager;
/// <summary>
/// Gets the collection of loaded scenes in the editor.
/// </summary>
public ReadOnlyObservableCollection<SceneNode> LoadedScenes { get; }
/// <summary>
/// Event raised when a scene is loaded.
/// </summary>
public event Action<SceneNode>? OnSceneLoaded;
/// <summary>
/// Event raised when a scene is unloaded.
/// </summary>
public event Action<SceneNode>? OnSceneUnloaded;
/// <summary>
/// Event raised when an entity node is created.
/// </summary>
public event Action<EntityNode>? OnEntityNodeCreated;
/// <summary>
/// Event raised when an entity node is destroyed.
/// </summary>
public event Action<EntityNode>? OnEntityNodeDestroyed;
public EditorWorldManager(World editorWorld, SceneManager sceneManager)
{
_editorWorld = editorWorld;
_sceneManager = sceneManager;
_loadedScenes = [];
_sceneIdToNode = [];
_entityToNode = [];
LoadedScenes = new ReadOnlyObservableCollection<SceneNode>(_loadedScenes);
}
/// <summary>
/// Creates a new empty scene in the editor.
/// </summary>
/// <param name="name">The name of the scene.</param>
/// <returns>The created scene node.</returns>
public SceneNode CreateNewScene(string name = "New Scene")
{
var runtimeScene = _sceneManager.CreateScene();
var sceneNode = new SceneNode(runtimeScene, name)
{
IsLoaded = true
};
_loadedScenes.Add(sceneNode);
_sceneIdToNode[runtimeScene.ID] = sceneNode;
OnSceneLoaded?.Invoke(sceneNode);
return sceneNode;
}
/// <summary>
/// Unloads a scene from the editor.
/// </summary>
/// <param name="sceneNode">The scene to unload.</param>
public void UnloadScene(SceneNode sceneNode)
{
if (!_loadedScenes.Contains(sceneNode))
{
return;
}
// Remove all entity nodes from tracking
foreach (var entityNode in sceneNode.GetAllEntities().ToList())
{
_entityToNode.Remove(entityNode.Entity);
OnEntityNodeDestroyed?.Invoke(entityNode);
}
// Unload runtime scene
_sceneManager.UnloadScene(sceneNode.Scene);
// Remove from loaded scenes
_loadedScenes.Remove(sceneNode);
_sceneIdToNode.Remove(sceneNode.Scene.ID);
sceneNode.IsLoaded = false;
OnSceneUnloaded?.Invoke(sceneNode);
}
/// <summary>
/// Creates an entity in the specified scene.
/// </summary>
/// <param name="sceneNode">The scene to create the entity in.</param>
/// <param name="name">The display name of the entity.</param>
/// <param name="parent">Optional parent entity node.</param>
/// <returns>The created entity node.</returns>
public EntityNode CreateEntity(SceneNode sceneNode, string name = "Entity", EntityNode? parent = null)
{
// Create runtime entity with SceneID component
var entity = _editorWorld.EntityManager.CreateEntity();
_editorWorld.EntityManager.AddComponent(entity, new SceneID { id = sceneNode.Scene.ID });
// Create entity node
var entityNode = new EntityNode(entity, name);
// Add to scene graph
if (parent != null)
{
parent.AddChild(entityNode);
// Add Hierarchy component
_editorWorld.EntityManager.AddComponent(entity, new Hierarchy
{
parent = parent.Entity,
firstChild = Entity.Invalid,
nextSibling = Entity.Invalid
});
}
else
{
sceneNode.AddRootEntity(entityNode);
// Add root hierarchy component
_editorWorld.EntityManager.AddComponent(entity, Hierarchy.Root);
}
// Track entity node
_entityToNode[entity] = entityNode;
OnEntityNodeCreated?.Invoke(entityNode);
return entityNode;
}
/// <summary>
/// Destroys an entity and its node from the scene.
/// </summary>
/// <param name="entityNode">The entity node to destroy.</param>
public void DestroyEntity(EntityNode entityNode)
{
// Remove from parent or scene root
if (entityNode.Parent != null)
{
entityNode.Parent.RemoveChild(entityNode);
}
else if (entityNode.OwnerScene != null)
{
entityNode.OwnerScene.RemoveRootEntity(entityNode);
}
// Destroy all children recursively
var childrenCopy = entityNode.Children.ToList();
foreach (var child in childrenCopy)
{
DestroyEntity(child);
}
// Destroy runtime entity
_editorWorld.EntityManager.DestroyEntity(entityNode.Entity);
// Remove from tracking
_entityToNode.Remove(entityNode.Entity);
OnEntityNodeDestroyed?.Invoke(entityNode);
}
/// <summary>
/// Gets the scene node for a runtime scene ID.
/// </summary>
/// <param name="sceneId">The scene ID.</param>
/// <returns>The scene node if found, null otherwise.</returns>
public SceneNode? GetSceneNode(short sceneId)
{
_sceneIdToNode.TryGetValue(sceneId, out var sceneNode);
return sceneNode;
}
/// <summary>
/// Gets the entity node for a runtime entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>The entity node if found, null otherwise.</returns>
public EntityNode? GetEntityNode(Entity entity)
{
_entityToNode.TryGetValue(entity, out var entityNode);
return entityNode;
}
/// <summary>
/// Rebuilds the scene graph from the current world state.
/// Useful after loading a scene from disk.
/// </summary>
/// <param name="sceneNode">The scene node to rebuild.</param>
public void RebuildSceneGraph(SceneNode sceneNode)
{
// Clear existing nodes
sceneNode.RootEntities.Clear();
// Build query for entities in this scene
var builder = new QueryBuilder();
builder.WithAll([ComponentTypeID<SceneID>.Value, ComponentTypeID<Hierarchy>.Value]);
var queryID = builder.Build(_editorWorld);
ref var query = ref _editorWorld.ComponentManager.GetEntityQueryReference(queryID);
// First pass: Create all entity nodes
var entityNodes = new Dictionary<Entity, EntityNode>();
foreach (var chunk in query.GetChunkIterator())
{
var entities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<SceneID>();
for (int i = 0; i < chunk.Count; i++)
{
if (sceneIDs[i].id == sceneNode.Scene.ID)
{
var entity = entities[i];
// Try to get existing node name or use default
var name = _entityToNode.TryGetValue(entity, out var existing)
? existing.Name
: "Entity";
var entityNode = new EntityNode(entity, name);
entityNodes[entity] = entityNode;
_entityToNode[entity] = entityNode;
}
}
}
// Second pass: Build hierarchy
foreach (var chunk in query.GetChunkIterator())
{
var entities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<SceneID>();
var hierarchies = chunk.GetComponentData<Hierarchy>();
for (int i = 0; i < chunk.Count; i++)
{
if (sceneIDs[i].id == sceneNode.Scene.ID)
{
var entity = entities[i];
var hierarchy = hierarchies[i];
var entityNode = entityNodes[entity];
if (hierarchy.parent.IsValid && entityNodes.TryGetValue(hierarchy.parent, out var parentNode))
{
parentNode.AddChild(entityNode);
}
else
{
sceneNode.AddRootEntity(entityNode);
}
}
}
}
}
}

View File

@@ -1,144 +1,10 @@
using Ghost.Entities;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
/// <summary>
/// Represents an entity node in the editor scene graph hierarchy.
/// Contains editor-only metadata like display name and hierarchy information.
/// </summary>
public class EntityNode
public sealed partial class EntityNode : SceneGraphNode
{
private string _name;
private readonly ObservableCollection<EntityNode> _children;
private readonly Entity _entity;
/// <summary>
/// Gets or sets the entity this node represents.
/// </summary>
public Entity Entity { get; set; }
/// <summary>
/// Gets or sets the display name for this entity in the editor.
/// This is NOT stored in runtime components.
/// </summary>
public string Name
{
get => _name;
set
{
_name = value;
OnNameChanged?.Invoke(this);
}
}
/// <summary>
/// Gets the parent node of this entity node.
/// </summary>
public EntityNode? Parent { get; internal set; }
/// <summary>
/// Gets the scene node that contains this entity.
/// </summary>
public SceneNode? OwnerScene { get; internal set; }
/// <summary>
/// Gets the collection of child entity nodes.
/// </summary>
public ObservableCollection<EntityNode> Children => _children;
/// <summary>
/// Event raised when the name property changes.
/// </summary>
public event Action<EntityNode>? OnNameChanged;
/// <summary>
/// Event raised when children collection changes.
/// </summary>
public event Action<EntityNode>? OnChildrenChanged;
public EntityNode(Entity entity, string name = "Entity")
{
Entity = entity;
_name = name;
_children = [];
_children.CollectionChanged += (s, e) => OnChildrenChanged?.Invoke(this);
}
/// <summary>
/// Adds a child entity node to this node.
/// </summary>
/// <param name="child">The child node to add.</param>
public void AddChild(EntityNode child)
{
if (child.Parent != null)
{
child.Parent.RemoveChild(child);
}
child.Parent = this;
child.OwnerScene = OwnerScene;
_children.Add(child);
}
/// <summary>
/// Removes a child entity node from this node.
/// </summary>
/// <param name="child">The child node to remove.</param>
/// <returns>True if the child was removed, false otherwise.</returns>
public bool RemoveChild(EntityNode child)
{
if (_children.Remove(child))
{
child.Parent = null;
return true;
}
return false;
}
/// <summary>
/// Gets all descendants of this node (children, grandchildren, etc.) in depth-first order.
/// </summary>
/// <returns>An enumerable of all descendant nodes.</returns>
public IEnumerable<EntityNode> GetAllDescendants()
{
foreach (var child in _children)
{
yield return child;
foreach (var descendant in child.GetAllDescendants())
{
yield return descendant;
}
}
}
/// <summary>
/// Finds an entity node by its entity reference.
/// </summary>
/// <param name="entity">The entity to search for.</param>
/// <returns>The entity node if found, null otherwise.</returns>
public EntityNode? FindNode(Entity entity)
{
if (Entity.Equals(entity))
{
return this;
}
foreach (var child in _children)
{
var found = child.FindNode(entity);
if (found != null)
{
return found;
}
}
return null;
}
public override string ToString()
{
return $"{Name} (Entity: {Entity})";
}
public Entity Entity => _entity;
}

View File

@@ -1,182 +0,0 @@
# Scene Graph System Implementation Summary
All planned features from the `SceneGraph Plan.md` have been implemented successfully. Here's what was created:
## 1. Runtime Types (Ghost.Engine)
### Scene.cs
- **Scene struct**: Lightweight runtime identifier for scenes
- **SceneManager class**: Manages scene lifecycle in a world
- `CreateScene()`: Creates new scenes
- `UnloadScene()`: Destroys all entities in a scene
- `GetSceneEntities()`: Queries entities by scene ID
Key design: Minimal runtime footprint, no metadata stored.
## 2. Editor Data Structures (Ghost.Editor.Core)
### SceneNode.cs
- Editor-only scene representation
- Properties:
- `Name`: Display name (NOT in runtime)
- `FilePath`: Where scene is saved
- `IsLoaded`, `IsDirty`: Editor state tracking
- `RootEntities`: Observable collection of root entity nodes
- Methods for managing root entities and finding nodes
### EntityNode.cs
- Editor-only entity representation
- Properties:
- `Name`: Display name (NOT in runtime)
- `Parent`, `Children`: Editor hierarchy
- `OwnerScene`: Back-reference to scene
- Methods for hierarchy management (AddChild, RemoveChild, FindNode, GetAllDescendants)
Both classes use `ObservableCollection` for WinUI 3 TreeView binding support.
## 3. Editor World Management
### EditorWorldManager.cs
- Central manager for editor world and scene graph
- Features:
- Scene creation/unloading
- Entity creation/destruction with automatic SceneID assignment
- Hierarchy component management
- Entity/Scene node tracking via dictionaries
- `RebuildSceneGraph()`: Reconstructs scene graph from world state after loading
Maintains bidirectional mapping: Entity ↔ EntityNode, SceneID ↔ SceneNode
## 4. Serialization (Ghost.Editor.Core)
### SceneData.cs
JSON data structures:
- `SceneData`: Contains name + list of entities
- `EntityData`: Name, parent local ID, components dictionary
- `ComponentData`: Type name + fields dictionary
**Key Feature**: Entities are ordered by file-local ID (index in list).
### SceneSerializer.cs
Complete save/load implementation with entity reference remapping:
**Save Process**:
1. Build `Entity → FileLocalID` mapping
2. Serialize each entity's components
3. **Entity references converted to file-local IDs**
4. Validate no cross-scene references (throws exception if found)
5. Write JSON to file
**Load Process**:
1. Read JSON
2. Create all entities first (two-pass)
3. Build `FileLocalID → Entity` mapping
4. Deserialize components, **remapping file-local IDs back to global Entity IDs**
5. Rebuild hierarchy
Uses reflection to serialize/deserialize component fields. Entity references are detected by field type and specially handled.
## 5. Hierarchy System (Ghost.Engine)
### HierarchyUtility.cs
Runtime utilities for working with Hierarchy component:
- `SetParent()`: Update parent-child relationships, maintains sibling lists
- `GetParent()`: Query parent
- `GetChildren()`: Get immediate children
- `GetDescendants()`: Recursive depth-first traversal
- `IsAncestor()`: Check ancestor relationship
Uses the existing `Hierarchy` component (parent, firstChild, nextSibling pattern).
## 6. Validation (Ghost.Editor.Core)
### SceneValidator.cs
Validates scene integrity:
**ValidateSceneReferences**:
- Checks all Entity fields in components
- Ensures referenced entities exist
- **Enforces same-scene constraint** (errors on cross-scene refs)
**ValidateHierarchy**:
- Detects circular parent-child loops
- Warns if parent is outside scene
Returns `ValidationResult` with errors/warnings lists.
## Architecture Highlights
### Runtime vs Editor Separation
- Runtime (`Ghost.Engine`): Only SceneID component, SceneManager, HierarchyUtility
- Editor (`Ghost.Editor.Core`): All metadata (names), scene graph tree, serialization
- **Zero runtime overhead** for editor-only data
### Entity Reference Remapping
Per plan, entity references use **file-local IDs** in saved scenes:
- Avoids brittle global IDs
- Enables scene prefabs/templates in future
- Automatically remapped on load
### Cross-Scene Reference Prevention
- Validation layer enforces no cross-references
- SerializeComponent throws exception if detected during save
- Plan notes: Use queries/singletons for cross-scene access
### JSON for Editor, Binary for Runtime
- Current impl: JSON serialization for editor (SceneSerializer.cs)
- Plan notes MemoryPack for runtime binary format
- Reflection allowed in editor, AOT-compatible runtime preserved
## Integration Points
### Existing Components Used
- `SceneID` (Ghost.Engine/Components/SceneID.cs) - scene membership tag
- `Hierarchy` (Ghost.Engine/Components/Hierarchy.cs) - parent-child structure
- `LocalToWorld` - transform (referenced but not modified)
### ECS Integration
- Uses `QueryBuilder` and chunk iteration for queries
- `EntityManager` for all entity operations
- Archetype layouts for component enumeration
## What's NOT Implemented (As Per Plan)
The plan explicitly states:
> "Leave the actual UI implementation (TreeView) for later"
Not included in this implementation:
- WinUI 3 TreeView XAML
- UI controls for hierarchy panel
- Drag-drop entity reparenting
- Context menus
- Icons/gizmos
These are left for UI layer implementation. The data structures (SceneNode, EntityNode with ObservableCollection) are designed to bind directly to TreeView.
## Files Created
**Ghost.Engine:**
- `Scene.cs` - Scene struct + SceneManager
- `Systems/HierarchyUtility.cs` - Hierarchy helpers
**Ghost.Editor.Core:**
- `SceneGraph/SceneNode.cs` - Scene graph node
- `SceneGraph/EntityNode.cs` - Entity graph node
- `SceneGraph/EditorWorldManager.cs` - Central manager
- `Serialization/SceneData.cs` - JSON data structures
- `Serialization/SceneSerializer.cs` - Save/load logic
- `Validation/SceneValidator.cs` - Integrity checks
Total: 7 new files, ~1400 lines of code
## Next Steps
To complete the scene graph feature:
1. **Create WinUI 3 TreeView** bound to `EditorWorldManager.LoadedScenes`
2. **Add toolbar**: New Scene, Save Scene, Unload Scene buttons
3. **Entity creation UI**: Right-click menu, "Create Empty" button
4. **Drag-drop**: Reparent entities by dragging in TreeView
5. **Rename**: Double-click to rename EntityNode.Name
6. **Integration**: Wire EditorWorldManager into main editor app lifecycle
All the core logic is complete and follows the plan precisely.

View File

@@ -4,26 +4,31 @@ The Scene Graph is a hierarchical structure that represents all the objects and
## 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. Name stored here, not runtime component.
2. **Scene Node**: Represents a Scene object, which can contain multiple entities. Name stored here not runtime data.
There should be three main types of nodes in the Scene Graph for now:
1. **Scene Graph Node**: The base class for all nodes in the Scene Graph.
2. **Entity Node**: Represents an individual entity within a scene. Name stored here, not runtime component.
3. **Scene Node**: Represents a Scene object, which can contain multiple entities. Name stored here not runtime data.
### 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.
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.
- 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
@@ -38,28 +43,32 @@ An example hierarchy could look like this:
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.
> 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.
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:
- 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
> The name of the saved scene file should match the name of the scene node in the editor.
The scene data should be stored in a structured format (e.g., JSON or binary) that includes:
- 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
> The name of the saved scene file should match the name of the scene node in the editor.
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.
@@ -68,10 +77,11 @@ 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.
## What you need to implement
- Scene type for the runtime representation
- Scene Graph data structures (SceneNode, EntityNode)
- Editor World management (loading/unloading scenes, managing entities)
- Scene saving/loading logic with file local ID remapping
- Serialization/deserialization logic for scene data (JSON for editor, binary for runtime)
- Leave the actual UI implementation (TreeView) for later, focus on the data structures and logic first but make sure the data structures are compatible with TreeView binding in WinUI 3.
## What need to implement
- [ ] Scene type for the runtime representation if needed
- [ ] Scene Graph data structures (SceneNode, EntityNode)
- [ ] Editor World management (loading/unloading scenes, managing entities)
- [ ] Scene saving/loading logic with file local ID remapping
- [ ] Serialization/deserialization logic for scene data (JSON for editor, binary for runtime)
- [ ] UI integration for displaying and managing the Scene Graph in the editor with WinUI 3 TreeView

View File

@@ -0,0 +1,18 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
public abstract partial class SceneGraphNode : ObservableObject
{
[ObservableProperty]
public partial string Name
{
get; set;
}
public ObservableCollection<SceneGraphNode> Children
{
get;
} = new();
}

View File

@@ -1,154 +1,5 @@
using Ghost.Engine.Core;
using Ghost.Entities;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
/// <summary>
/// Represents a scene node in the editor scene graph hierarchy.
/// Contains editor-only metadata like display name and root entities.
/// </summary>
public class SceneNode
public sealed partial class SceneNode : SceneGraphNode
{
private string _name;
private readonly ObservableCollection<EntityNode> _rootEntities;
/// <summary>
/// Gets or sets the runtime scene this node represents.
/// </summary>
public Scene Scene { get; set; }
/// <summary>
/// Gets or sets the display name for this scene in the editor.
/// This is NOT stored in runtime data.
/// </summary>
public string Name
{
get => _name;
set
{
_name = value;
OnNameChanged?.Invoke(this);
}
}
/// <summary>
/// Gets or sets the file path where this scene is saved.
/// </summary>
public string? FilePath { get; set; }
/// <summary>
/// Gets or sets whether this scene is currently loaded in the editor.
/// </summary>
public bool IsLoaded { get; set; }
/// <summary>
/// Gets or sets whether this scene has unsaved changes.
/// </summary>
public bool IsDirty { get; set; }
/// <summary>
/// Gets the collection of root entity nodes in this scene.
/// </summary>
public ObservableCollection<EntityNode> RootEntities => _rootEntities;
/// <summary>
/// Event raised when the name property changes.
/// </summary>
public event Action<SceneNode>? OnNameChanged;
/// <summary>
/// Event raised when root entities collection changes.
/// </summary>
public event Action<SceneNode>? OnRootEntitiesChanged;
public SceneNode(Scene scene, string name = "New Scene")
{
Scene = scene;
_name = name;
_rootEntities = [];
_rootEntities.CollectionChanged += (s, e) => OnRootEntitiesChanged?.Invoke(this);
}
/// <summary>
/// Adds a root entity node to this scene.
/// </summary>
/// <param name="entityNode">The entity node to add.</param>
public void AddRootEntity(EntityNode entityNode)
{
// Remove from previous parent if any
if (entityNode.Parent != null)
{
entityNode.Parent.RemoveChild(entityNode);
}
entityNode.Parent = null;
entityNode.OwnerScene = this;
_rootEntities.Add(entityNode);
}
/// <summary>
/// Removes a root entity node from this scene.
/// </summary>
/// <param name="entityNode">The entity node to remove.</param>
/// <returns>True if the entity was removed, false otherwise.</returns>
public bool RemoveRootEntity(EntityNode entityNode)
{
if (_rootEntities.Remove(entityNode))
{
entityNode.OwnerScene = null;
return true;
}
return false;
}
/// <summary>
/// Gets all entity nodes in this scene (root and descendants) in depth-first order.
/// </summary>
/// <returns>An enumerable of all entity nodes in the scene.</returns>
public IEnumerable<EntityNode> GetAllEntities()
{
foreach (var root in _rootEntities)
{
yield return root;
foreach (var descendant in root.GetAllDescendants())
{
yield return descendant;
}
}
}
/// <summary>
/// Finds an entity node by its entity reference.
/// </summary>
/// <param name="entity">The entity to search for.</param>
/// <returns>The entity node if found, null otherwise.</returns>
public EntityNode? FindNode(Entity entity)
{
foreach (var root in _rootEntities)
{
var found = root.FindNode(entity);
if (found != null)
{
return found;
}
}
return null;
}
/// <summary>
/// Marks this scene as dirty (has unsaved changes).
/// </summary>
public void MarkDirty()
{
IsDirty = true;
}
public override string ToString()
{
return $"{Name} (Scene ID: {Scene.ID}, Entities: {_rootEntities.Count})";
}
}