diff --git a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj
index 19c8eb3..692cf07 100644
--- a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj
+++ b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj
@@ -16,10 +16,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs b/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs
index 941b2f3..c15075b 100644
--- a/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs
+++ b/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs
@@ -6,9 +6,16 @@ namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class EntityNode : SceneGraphNode
{
- private readonly Entity _entity;
+ public Entity Entity
+ {
+ get;
+ }
- public Entity Entity => _entity;
+ public EntityNode(World world, Entity entity, string name)
+ : base(world, name)
+ {
+ Entity = entity;
+ }
public override IconSource? CreateIcon()
{
@@ -27,19 +34,4 @@ public sealed partial class EntityNode : SceneGraphNode
{
throw new NotImplementedException();
}
-
- public override DataTemplate GetSceneHierarchyTemplate()
- {
- var template = @"
-
-
-
-
-
-
-
- ";
-
- return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
- }
}
diff --git a/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraph Implementation Plan.md b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraph Implementation Plan.md
new file mode 100644
index 0000000..861f9f4
--- /dev/null
+++ b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraph Implementation Plan.md
@@ -0,0 +1,521 @@
+# SceneGraph Implementation Plan
+
+## Summary of Design Decisions
+
+| Decision | Choice |
+|----------|--------|
+| Hierarchy data model | Linked-list (`parent`/`firstChild`/`nextSibling`), efficient for ECS |
+| Editor ↔ World relationship | Mirror — editor world holds ECS entities, SceneGraph reflects them as observable nodes |
+| Transform representation | `LocalToWorld` matrix only (no separate PRS components) |
+| Implementation order | Data structures → HierarchySystem → Mirror bridge → UI → Serialization |
+
+---
+
+## Phase 1: Fix SceneGraph Data Structures
+
+**Goal:** Make `EntityNode`, `SceneNode`, and `SceneGraphNode` correct, complete, and constructible.
+
+### 1.1 Enhance `SceneGraphNode`
+
+**File:** `SceneGraphNode.cs`
+
+- Add a `World` property so every node knows which world it belongs to.
+- Make the constructor accept `World` and `string name`.
+- Keep `ObservableObject` for MVVM binding.
+- Keep `Children` as `ObservableCollection`.
+
+```csharp
+public abstract partial class SceneGraphNode : ObservableObject, IInspectable
+{
+ public World World { get; }
+
+ [ObservableProperty]
+ public partial string Name { get; set; }
+
+ public ObservableCollection Children { get; } = new();
+
+ protected SceneGraphNode(World world, string name)
+ {
+ World = world;
+ Name = name;
+ }
+
+ // ... existing abstract members
+}
+```
+
+### 1.2 Enhance `SceneNode`
+
+**File:** `SceneNode.cs`
+
+- Add a `Scene` field referencing the runtime `Scene` struct.
+- Add a constructor: `SceneNode(World world, Scene scene, string name)`.
+- Remove `XamlReader.Load()` template generation (Phase 4 will move to XAML resources).
+
+```csharp
+public sealed partial class SceneNode : SceneGraphNode
+{
+ public Scene Scene { get; }
+
+ public SceneNode(World world, Scene scene, string name)
+ : base(world, name)
+ {
+ Scene = scene;
+ }
+
+ // icon, header, inspector, template
+}
+```
+
+### 1.3 Enhance `EntityNode`
+
+**File:** `EntityNode.cs`
+
+- Add a constructor: `EntityNode(World world, Entity entity, string name)`.
+- Remove `XamlReader.Load()` template.
+- Entity reference is read-only after construction (immutable node identity).
+
+```csharp
+public sealed partial class EntityNode : SceneGraphNode
+{
+ public Entity Entity { get; }
+
+ public EntityNode(World world, Entity entity, string name)
+ : base(world, name)
+ {
+ Entity = entity;
+ }
+
+ // icon, header, inspector, template
+}
+```
+
+### 1.4 Add `SceneGraphBuilder`
+
+**New file:** `SceneGraphBuilder.cs`
+
+Used for the **initial full build** of the scene graph from an ECS `World`. After initial construction, incremental updates are handled by `SceneGraphSyncService` (Phase 3).
+
+**Algorithm:**
+
+1. Query all entities with `SceneID` component.
+2. Group entities by `SceneID.scene.ID`.
+3. For each scene group:
+ - Create a `SceneNode` (name comes from editor metadata, not from runtime).
+ - Walk the `Hierarchy` component linked-list to build the tree:
+ - Root entities are those with `Hierarchy.parent == Entity.Invalid`.
+ - For each root, create an `EntityNode` and recursively visit `firstChild` → `nextSibling`.
+ - Entity names come from a naming system (either a component like `EntityName`, or editor-side metadata stored on the node).
+4. Return `ObservableCollection` (the list of root scenes).
+
+**Name resolution strategy:**
+
+Names are stored directly on `EntityNode.Name`. During initial build, all entities get default names. During incremental sync, existing nodes (and their user-assigned names) are preserved because nodes are matched by `Entity` identity rather than recreated. See Phase 1.5 and 3.2 for details.
+
+### 1.5 Entity Names (Editor-Only)
+
+**No runtime component.** Entity names have no purpose in the runtime ECS. Names live purely on the editor side, stored directly on `EntityNode.Name`.
+
+**Strategy:** Entity names persist naturally across syncs because `EntityNode` instances survive — the sync is incremental, not a full rebuild (see Phase 3.2). Names are read directly from `EntityNode.Name` for inspector display and serialization.
+
+- New entities get a default name (e.g., `"Entity"`).
+- User-renamed entities keep their name because the node instance is preserved.
+- Destroyed entities have their node removed (names disappear with them).
+- No dictionary, no cross-world identity concerns, no runtime footprint.
+
+---
+
+## Phase 2: HierarchySystem (Runtime)
+
+**Goal:** Maintain the `Hierarchy` component's linked-list invariants.
+
+**New file:** `Runtime/Ghost.Engine/Systems/HierarchySystem.cs`
+
+### 2.1 Operations
+
+The system exposes static helper methods (or instance methods on `EntityManager`) for hierarchy mutation. All mutations use `EntityCommandBuffer` internally to avoid structural changes mid-iteration.
+
+| Operation | Description |
+|-----------|-------------|
+| `SetParent(child, parent)` | Links `child` as a child of `parent`. Updates `parent`, `firstChild`, `nextSibling` on both sides. |
+| `RemoveParent(child)` | Unlinks `child` from its current parent. It becomes a root entity. |
+| `DestroyEntity(entity)` | Also unlinks from parent, reparents children to grandparent (or makes them roots). |
+
+### 2.2 `SetParent(Entity child, Entity parent)`
+
+**Preconditions:**
+- `child.IsValid` and `parent.IsValid`.
+- Both entities exist in the world.
+- Both have the `Hierarchy` component.
+- `child != parent` (no self-parenting).
+- `parent` is not a descendant of `child` (no cycles).
+
+**Algorithm:**
+
+1. If `child` already has a parent, call `RemoveParent(child)` first.
+2. Get `ref childHierarchy`, `ref parentHierarchy`.
+3. Set `childHierarchy.parent = parent`.
+4. Set `childHierarchy.nextSibling = parentHierarchy.firstChild`.
+5. Set `parentHierarchy.firstChild = child`.
+
+### 2.3 `RemoveParent(Entity child)`
+
+**Preconditions:** `child` has the `Hierarchy` component and has a parent.
+
+**Algorithm:**
+
+1. Get `ref childHierarchy`. Let `parent_entity = childHierarchy.parent`.
+2. Get `ref parentHierarchy`.
+3. Walk the sibling chain of `parentHierarchy.firstChild` to find and unlink `child`:
+ - If `firstChild == child`: set `firstChild = childHierarchy.nextSibling`.
+ - Else: walk `prev → current → nextSibling`, set `prev.nextSibling = childHierarchy.nextSibling`.
+4. Set `childHierarchy.parent = Entity.Invalid`.
+5. Set `childHierarchy.nextSibling = Entity.Invalid`.
+
+### 2.4 Entity Destruction Cleanup
+
+When an entity is destroyed via `EntityManager.DestroyEntity()`:
+
+1. Get its `Hierarchy` component.
+2. Call `RemoveParent(entity)`.
+3. **Cascade destroy all children:** Walk `firstChild` → `nextSibling` and recursively destroy every descendant entity. Destroy children before destroying the parent to ensure correct cleanup order.
+4. When saving/loading, this means saving an entity implies saving its entire subtree. Deleting an entity implies deleting its entire subtree.
+
+### 2.5 Validation
+
+Each method validates invariants in `DEBUG`/`GHOST_EDITOR` builds:
+- No entity appears as its own ancestor (cycle detection via ancestor walk).
+- `firstChild`/`parent`/`nextSibling` references are all valid entities or `Entity.Invalid`.
+- No dangling references.
+
+### 2.6 `HierarchySystem` as an `ISystem`
+
+The system does NOT run every frame automatically. Parent/child mutations are explicit (called from editor commands or loading code). The system's `Update()` is a no-op. It exists as an `ISystem` so it can:
+- Register its existence in the system graph.
+- Be queried via `World.SystemManager`.
+- Hold references to necessary queries.
+
+Alternatively: make it a static utility class that takes `World` as a parameter. **Recommendation:** static utility class (`HierarchyUtility`) to avoid entity archetype cost for a no-op system. The `Hierarchy` component itself drives the tree shape.
+
+---
+
+## Phase 3: Mirror Bridge (Editor ↔ ECS sync)
+
+**Goal:** Keep the `SceneGraphNode` tree in the editor synchronized with the ECS `World`.
+
+### 3.1 `EditorWorldService`
+
+**New file:** `Editor/Ghost.Editor.Core/Services/EditorWorldService.cs`
+
+- Registers as a singleton via `[EditorInjection(ServiceLifetime.Singleton)]`.
+- Creates the editor `World` on startup.
+- Holds the `World` reference.
+- Disposes the world on editor shutdown.
+
+```csharp
+[EditorInjection(ServiceLifetime.Singleton)]
+public class EditorWorldService : IDisposable
+{
+ public World EditorWorld { get; }
+
+ public EditorWorldService()
+ {
+ EditorWorld = World.Create(entityCapacity: 1024);
+ }
+
+ public void Dispose()
+ {
+ World.Destroy(EditorWorld.ID);
+ }
+}
+```
+
+### 3.2 Scene Graph Sync Strategy
+
+**Incremental sync via polling.** Full rebuild is avoided — existing nodes are matched by `Entity` identity and preserved across syncs. This keeps names, selection state, and expanded/collapsed state intact.
+
+**Algorithm (runs on timer, e.g., every 100ms):**
+
+1. Check `EditorWorld.Version` — if unchanged, skip.
+2. Query all entities with `SceneID` component from the editor world.
+3. Group by `SceneID.scene.ID`.
+4. **For each scene group:**
+ - Find or create the `SceneNode` in `RootNodes` (match by `Scene.ID`).
+ - Walk the `Hierarchy` linked-list of roots.
+ - **For each root entity:** find existing `EntityNode` in the tree by `Entity` identity. If found, keep the node (name, expansion, selection intact). If not found, create a new `EntityNode` with default name.
+ - **Recurse** into `firstChild` → `nextSibling`, matching existing nodes at each level.
+5. **Remove stale nodes:** Any `EntityNode` not matched in step 4 is destroyed (entity no longer exists in world). Remove these nodes (and their subtrees) from the tree.
+6. **Update hierarchy links:** If a matched entity's parent changed, move the `EntityNode` to its new parent's `Children` collection.
+
+**Key benefits of incremental sync:**
+- `EntityNode.Name` persists naturally — node instances survive.
+- No dictionary, no name map.
+- Selection and tree expansion state survive across syncs.
+- Only affected subtrees change — minimal UI churn.
+
+### 3.3 Incremental Sync Implementation
+
+**New file:** `Editor/Ghost.Editor.Core/Services/SceneGraphSyncService.cs`
+
+```csharp
+[EditorInjection(ServiceLifetime.Singleton)]
+public class SceneGraphSyncService
+{
+ private readonly EditorWorldService _worldService;
+ private uint _lastSyncedVersion;
+
+ public ObservableCollection RootNodes { get; } = new();
+
+ public void Tick()
+ {
+ var currentVersion = _worldService.EditorWorld.Version;
+ if (currentVersion == _lastSyncedVersion)
+ return;
+
+ _lastSyncedVersion = currentVersion;
+ SyncScenesAndEntities(_worldService.EditorWorld);
+ }
+
+ private void SyncScenesAndEntities(World world)
+ {
+ // 1. Query all entities with SceneID
+ // 2. Group by scene ID
+ // 3. For each scene: match/create SceneNode, walk Hierarchy linked-list,
+ // match/create EntityNode, remove stale nodes, update Children links
+ }
+}
+```
+
+### 3.4 Selection → Inspector Wiring
+
+Already partially wired:
+- `InspectorService.SetSelected(IInspectable, source)` exists.
+- `SceneGraphNode : IInspectable` exists.
+- Clicking a `TreeViewItem` sets selection → fires event → calls `SetSelected`.
+
+**To complete:**
+- In `Hierarchy.xaml.cs`, handle `TreeView.ItemInvoked` or selection changed.
+- Call `InspectorService.SetSelected(node, this)`.
+
+---
+
+## Phase 4: TreeView UI
+
+**Goal:** Replace `ListView` placeholder with a functional `TreeView` bound to the scene graph.
+
+### 4.1 Replace `ListView` with `TreeView`
+
+**File:** `Hierarchy.xaml`
+
+Changes:
+- Replace `` with ``.
+- Bind `ItemsSource` to `RootNodes` from `SceneGraphSyncService`.
+- Use a `DataTemplateSelector` that picks `SceneNode.GetSceneHierarchyTemplate()` or `EntityNode.GetSceneHierarchyTemplate()` based on node type.
+- Move templates to XAML resources (static `DataTemplate` in `Page.Resources` or a `ResourceDictionary`) instead of `XamlReader.Load()`.
+
+**Template selection:**
+
+```xml
+
+
+
+
+
+```
+
+### 4.2 Move Templates to XAML Resources
+
+Create a `ResourceDictionary` (or put in `App.xaml` / `Hierarchy.xaml.Resources`):
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 4.3 DataTemplateSelector
+
+**New file:** `Editor/Ghost.Editor/Views/Controls/SceneGraphTemplateSelector.cs`
+
+```csharp
+public class SceneGraphTemplateSelector : DataTemplateSelector
+{
+ public DataTemplate SceneNodeTemplate { get; set; }
+ public DataTemplate EntityNodeTemplate { get; set; }
+
+ protected override DataTemplate SelectTemplateCore(object item)
+ {
+ return item switch
+ {
+ SceneNode => SceneNodeTemplate,
+ EntityNode => EntityNodeTemplate,
+ _ => base.SelectTemplateCore(item)
+ };
+ }
+}
+```
+
+### 4.4 Remove `GetSceneHierarchyTemplate()` from Node Classes
+
+Once templates are in XAML resources (Phase 4.2), the `GetSceneHierarchyTemplate()` method on `SceneGraphNode` is no longer needed. Remove it from the abstract class and both subclasses. Do NOT remove it in Phase 1 — the method is still referenced until the XAML templates are ready.
+
+### 4.5 Context Menu Support
+
+Add `TreeViewItem.ContextFlyout` or right-click handling for:
+- **Scene-level:** Create Entity, Rename Scene, Unload Scene, Save Scene.
+- **Entity-level:** Create Child Entity, Delete Entity, Duplicate Entity, Rename Entity.
+
+Create entity commands go through `EntityCommandBuffer` on the editor world.
+
+### 4.6 Search/Filter
+
+Wire the search `TextBox` to filter the TreeView:
+- On text change, iterate `RootNodes` recursively.
+- If a node or any descendant matches, show it. Otherwise collapse/hide.
+- WinUI `TreeView` doesn't have built-in filtering, so implement a filtered copy of the tree or use `Visibility` toggling.
+
+### 4.7 Drag & Drop Reparenting (Stretch Goal)
+
+- Allow dragging an `EntityNode` onto another `EntityNode` to reparent.
+- Uses WinUI drag-and-drop APIs.
+- Calls `HierarchyUtility.SetParent()` on the editor world.
+- Scene graph refreshes via sync.
+
+---
+
+## Phase 5: Serialization (JSON Editor / Binary Runtime)
+
+### 5.1 File Local ID Scheme
+
+When serializing a scene, entities are ordered (index in the list = file-local ID). All `Entity` references within components are serialized as file-local IDs, not global entity IDs.
+
+**Rationale:** Global entity IDs are unpredictable across loads. File-local IDs are deterministic (they are just the list index) and remapped on load.
+
+### 5.2 Serialization Format
+
+**JSON (Editor) — in `Ghost.Editor.Core`:**
+
+```json
+{
+ "name": "MainScene",
+ "entities": [
+ {
+ "components": {
+ "Ghost.Engine.Components.Hierarchy": {
+ "parent": -1,
+ "firstChild": 1,
+ "nextSibling": -1
+ },
+ "Ghost.Engine.Components.LocalToWorld": {
+ "matrix": { "m00": 1.0, ... }
+ }
+ }
+ },
+ {
+ "components": { ... }
+ }
+ ]
+}
+```
+
+- Entity index in the array = file-local ID.
+- `Entity` references (`parent`, `firstChild`, `nextSibling`) stored as `int` file-local IDs. `-1` = `Entity.Invalid`.
+- Component types stored by their stable `FullName` string.
+- Uses `System.Text.Json` with reflection (allowed in editor).
+
+**Binary (Runtime) — in `Ghost.Engine`:**
+
+- MemoryPack serialization of the same structure.
+- Must be AOT-compatible (use source-generated formatters).
+- Components are blittable, so MemoryPack handles them efficiently.
+
+### 5.3 Save Algorithm
+
+1. Get all entities with `SceneID == targetScene` via `SceneManager.GetSceneEntities()`.
+2. Sort entities in a deterministic order (e.g., by hierarchy depth-first traversal for deterministic output).
+3. Create a `fileLocalID → Entity` map (list index → entity).
+4. Create a reverse `Entity → fileLocalID` map.
+5. For each entity, serialize all components.
+6. For any component field of type `Entity`, replace with the file-local ID (using the reverse map).
+7. For `ManagedEntityRef` / script data, serialize via MemoryPack.
+8. Write to file.
+
+### 5.4 Load Algorithm
+
+1. Deserialize JSON/binary → list of entity component data.
+2. Allocate all entities in the target `World` (no components yet, or minimal archetype).
+3. Build `fileLocalID → Entity` map (list index → new global entity).
+4. For each entity:
+ - Add its components to the entity.
+ - For any `Entity`-typed field, look up the file-local ID in the map and replace with the new global entity.
+5. Call `HierarchyUtility` to validate/repair hierarchy invariants if needed.
+6. Return the count of loaded entities.
+
+### 5.5 References to Other Scenes
+
+Per the architecture plan, cross-scene references are not supported. If a component references an entity from another scene:
+- On save: log a warning and serialize as `-1` (invalid).
+- On load: the reference will be `Entity.Invalid`.
+
+### 5.6 File Naming
+
+Scene files use the `.gscene` extension (`g` = GhostEngine):
+- `Assets/Scenes/{SceneName}.gscene.json` (editor JSON)
+- `Assets/Scenes/{SceneName}.gscene` (runtime binary)
+
+**Scene name resolution:**
+- The scene's name derives from the file name (minus extension). E.g., `MyScene.gscene` → name is `"MyScene"`.
+- For unsaved/new scenes: default name is `"NewScene"`.
+- `SceneNode.Name` is set from the file name on load, and used as the save target on save.
+
+---
+
+## Component Checklist
+
+| Phase | Task | File(s) |
+|-------|------|---------|
+| **1.1** | Enhance `SceneGraphNode` — add `World`, constructor | `SceneGraphNode.cs` |
+| **1.2** | Enhance `SceneNode` — add `Scene` field, constructor | `SceneNode.cs` |
+| **1.3** | Enhance `EntityNode` — add constructor, make usable | `EntityNode.cs` |
+| **1.4** | Add `SceneGraphBuilder` | New: `SceneGraphBuilder.cs` |
+| **1.5** | Entity name strategy (editor-only, no component) | See Section 1.4 |
+| **2** | Add `HierarchyUtility` static class | New: `Runtime/.../HierarchyUtility.cs` |
+| **3.1** | Add `EditorWorldService` | New: `Editor/.../EditorWorldService.cs` |
+| **3.2-3** | Add `SceneGraphSyncService` | New: `Editor/.../SceneGraphSyncService.cs` |
+| **4.1** | Replace `ListView` with `TreeView` in XAML | `Hierarchy.xaml` |
+| **4.2** | Move templates to XAML resources | `Hierarchy.xaml` |
+| **4.3** | Add `SceneGraphTemplateSelector` | New: `Views/Controls/SceneGraphTemplateSelector.cs` |
+| **4.4** | Remove `GetSceneHierarchyTemplate()` methods | `SceneGraphNode.cs`, `.cs`, `.cs` |
+| **4.5** | Context menu (create/delete entity) | `Hierarchy.xaml.cs` |
+| **4.6** | Search/filter | `Hierarchy.xaml.cs` |
+| **4.7** | Drag-drop reparenting (stretch) | `Hierarchy.xaml.cs` |
+| **5.1-5** | Scene save/load with ID remapping | New files in `Serialization/` |
+| **5.5** | MemoryPack source-gen formatters | `Ghost.Engine` |
+
+---
+
+## Resolved Questions
+
+| # | Question | Decision |
+|---|----------|----------|
+| 1 | Entity name storage | **Editor-only** — names live on `EntityNode.Name` directly. Persist across syncs via incremental matching by `Entity` identity (no dictionary needed). No runtime component. |
+| 2 | Orphan behavior on parent destroy | **Cascade destroy** all children recursively. |
+| 3 | Remove `GetSceneHierarchyTemplate()` timing | **Phase 4** — keep until XAML templates are in place. |
+| 4 | Scene name persistence | **File name** — name = file name minus `.gscene` extension. Unsaved scenes default to `"NewScene"`. |
diff --git a/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraphBuilder.cs b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraphBuilder.cs
new file mode 100644
index 0000000..a162696
--- /dev/null
+++ b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraphBuilder.cs
@@ -0,0 +1,156 @@
+using Ghost.Engine.Components;
+using Ghost.Engine.Core;
+using Ghost.Entities;
+
+namespace Ghost.Editor.Core.SceneGraph;
+
+public static class SceneGraphBuilder
+{
+ public static List Build(World world)
+ {
+ var sceneNodes = new List();
+ var sceneEntities = GroupEntitiesByScene(world);
+
+ foreach (var (scene, entities) in sceneEntities)
+ {
+ var sceneName = GetDefaultSceneName(scene);
+ var sceneNode = new SceneNode(world, scene, sceneName);
+ BuildEntityTree(entities, sceneNode);
+ sceneNodes.Add(sceneNode);
+ }
+
+ return sceneNodes;
+ }
+
+ private static Dictionary> GroupEntitiesByScene(World world)
+ {
+ var sceneMap = new Dictionary>();
+ var queryID = new QueryBuilder().WithAll().Build(world);
+ ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
+
+ foreach (var chunk in query.GetChunkIterator())
+ {
+ var entities = chunk.GetEntities();
+ var sceneIDs = chunk.GetComponentData();
+
+ for (var i = 0; i < chunk.EntityCount; i++)
+ {
+ var s = sceneIDs[i].scene;
+ if (!s.IsValid)
+ {
+ continue;
+ }
+
+ if (!sceneMap.TryGetValue(s, out var list))
+ {
+ list = new List();
+ sceneMap[s] = list;
+ }
+
+ list.Add(entities[i]);
+ }
+ }
+
+ return sceneMap;
+ }
+
+ private static void BuildEntityTree(List entities, SceneGraphNode parentNode)
+ {
+ var entitySet = new HashSet(entities);
+ var childrenByParent = new Dictionary>();
+ var roots = new List();
+
+ foreach (var entity in entities)
+ {
+ Hierarchy hierarchy = default;
+ var hasHierarchy = TryGetHierarchyComponent(parentNode.World, entity, ref hierarchy);
+
+ if (hasHierarchy && hierarchy.parent.IsValid && entitySet.Contains(hierarchy.parent))
+ {
+ if (!childrenByParent.TryGetValue(hierarchy.parent, out var list))
+ {
+ list = new List();
+ childrenByParent[hierarchy.parent] = list;
+ }
+
+ list.Add(entity);
+ }
+ else
+ {
+ roots.Add(entity);
+ }
+ }
+
+ foreach (var rootEntity in roots)
+ {
+ var entityNode = new EntityNode(parentNode.World, rootEntity, "Entity");
+ parentNode.Children.Add(entityNode);
+ BuildSubtree(entityNode, childrenByParent);
+ }
+ }
+
+ private static void BuildSubtree(EntityNode parentNode, Dictionary> childrenByParent)
+ {
+ if (!childrenByParent.TryGetValue(parentNode.Entity, out var childList))
+ {
+ return;
+ }
+
+ Hierarchy parentHierarchy = default;
+ if (!TryGetHierarchyComponent(parentNode.World, parentNode.Entity, ref parentHierarchy))
+ {
+ foreach (var childEntity in childList)
+ {
+ var childNode = new EntityNode(parentNode.World, childEntity, "Entity");
+ parentNode.Children.Add(childNode);
+ BuildSubtree(childNode, childrenByParent);
+ }
+
+ return;
+ }
+
+ var sibling = parentHierarchy.firstChild;
+ while (sibling.IsValid)
+ {
+ if (childList.Contains(sibling))
+ {
+ var childNode = new EntityNode(parentNode.World, sibling, "Entity");
+ parentNode.Children.Add(childNode);
+ BuildSubtree(childNode, childrenByParent);
+ }
+
+ Hierarchy siblingHierarchy = default;
+ if (!TryGetHierarchyComponent(parentNode.World, sibling, ref siblingHierarchy))
+ {
+ break;
+ }
+
+ sibling = siblingHierarchy.nextSibling;
+ }
+ }
+
+ private static unsafe bool TryGetHierarchyComponent(World world, Entity entity, ref Hierarchy hierarchy)
+ {
+ var location = world.EntityManager.GetEntityLocation(entity);
+ if (!location.IsSuccess)
+ {
+ return false;
+ }
+
+ ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.Value.archetypeID);
+ var hierarchyID = ComponentTypeID.Value;
+ var pData = archetype.GetComponentData(location.Value.chunkIndex, location.Value.rowIndex, hierarchyID);
+ if (pData == null)
+ {
+ return false;
+ }
+
+ hierarchy = *(Hierarchy*)pData;
+ return true;
+ }
+
+ private static string GetDefaultSceneName(Scene scene)
+ {
+ return $"NewScene ({scene.ID})";
+ }
+}
diff --git a/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs
index cda5cca..3670a2a 100644
--- a/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs
+++ b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs
@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Contracts;
+using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;
@@ -14,14 +15,23 @@ public abstract partial class SceneGraphNode : ObservableObject, IInspectable
get; set;
}
+ public World World
+ {
+ get;
+ }
+
public ObservableCollection Children
{
get;
} = new();
+ protected SceneGraphNode(World world, string name)
+ {
+ World = world;
+ Name = name;
+ }
+
public abstract IconSource? CreateIcon();
public abstract UIElement? CreateHeader();
public abstract UIElement? CreateInspector();
-
- public abstract DataTemplate GetSceneHierarchyTemplate();
}
diff --git a/src/Editor/Ghost.Editor.Core/SceneGraph/SceneNode.cs b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneNode.cs
index 7ce7af7..d1ff50b 100644
--- a/src/Editor/Ghost.Editor.Core/SceneGraph/SceneNode.cs
+++ b/src/Editor/Ghost.Editor.Core/SceneGraph/SceneNode.cs
@@ -1,3 +1,5 @@
+using Ghost.Engine.Core;
+using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -5,6 +7,17 @@ namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class SceneNode : SceneGraphNode
{
+ public Scene Scene
+ {
+ get;
+ }
+
+ public SceneNode(World world, Scene scene, string name)
+ : base(world, name)
+ {
+ Scene = scene;
+ }
+
public override IconSource? CreateIcon()
{
return new FontIconSource
@@ -13,7 +26,6 @@ public sealed partial class SceneNode : SceneGraphNode
};
}
- // TODO: Implement custom header and inspector UI for the SceneNode
public override UIElement? CreateHeader()
{
return null;
@@ -23,23 +35,4 @@ public sealed partial class SceneNode : SceneGraphNode
{
return null;
}
-
- public override DataTemplate GetSceneHierarchyTemplate()
- {
- var template = @"
-
-
-
-
-
-
-
- ";
-
- return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
- }
}
\ No newline at end of file
diff --git a/src/Editor/Ghost.Editor.Core/Services/EditorWorldService.cs b/src/Editor/Ghost.Editor.Core/Services/EditorWorldService.cs
new file mode 100644
index 0000000..71f86ec
--- /dev/null
+++ b/src/Editor/Ghost.Editor.Core/Services/EditorWorldService.cs
@@ -0,0 +1,54 @@
+using Ghost.Editor.Core.SceneGraph;
+using Ghost.Entities;
+using Ghost.Engine.Core;
+using System.Collections.ObjectModel;
+
+namespace Ghost.Editor.Core.Services;
+
+[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(EditorWorldService))]
+public class EditorWorldService : IDisposable
+{
+ private const int DEFAULT_ENTITY_CAPACITY = 1024;
+
+ public World EditorWorld
+ {
+ get;
+ }
+
+ public ObservableCollection RootNodes
+ {
+ get;
+ } = new();
+
+ public EditorWorldService()
+ {
+ EditorWorld = World.Create(entityCapacity: DEFAULT_ENTITY_CAPACITY);
+ CreateDefaultScene();
+ RebuildSceneGraph();
+ }
+
+ public void CreateDefaultScene()
+ {
+ var scene = SceneManager.CreateScene();
+ var entity = EditorWorld.EntityManager.CreateEntity();
+ EditorWorld.EntityManager.AddComponent(entity, new Ghost.Engine.Components.SceneID
+ {
+ scene = scene
+ });
+ }
+
+ public void RebuildSceneGraph()
+ {
+ RootNodes.Clear();
+ var sceneNodes = SceneGraphBuilder.Build(EditorWorld);
+ foreach (var node in sceneNodes)
+ {
+ RootNodes.Add(node);
+ }
+ }
+
+ public void Dispose()
+ {
+ World.Destroy(EditorWorld.ID);
+ }
+}
diff --git a/src/Editor/Ghost.Editor.Core/Services/SceneGraphSyncService.cs b/src/Editor/Ghost.Editor.Core/Services/SceneGraphSyncService.cs
new file mode 100644
index 0000000..0469afb
--- /dev/null
+++ b/src/Editor/Ghost.Editor.Core/Services/SceneGraphSyncService.cs
@@ -0,0 +1,224 @@
+using Ghost.Editor.Core.SceneGraph;
+using Ghost.Engine;
+using Ghost.Engine.Components;
+using Ghost.Engine.Core;
+using Ghost.Entities;
+
+namespace Ghost.Editor.Core.Services;
+
+[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(SceneGraphSyncService))]
+public class SceneGraphSyncService
+{
+ private readonly EditorWorldService _worldService;
+ private uint _lastSyncedVersion;
+
+ public SceneGraphSyncService(EditorWorldService worldService)
+ {
+ _worldService = worldService;
+ }
+
+ public bool Tick()
+ {
+ var currentVersion = _worldService.EditorWorld.Version;
+ if (currentVersion == _lastSyncedVersion)
+ {
+ return false;
+ }
+
+ _lastSyncedVersion = currentVersion;
+ SyncScenesAndEntities(_worldService.EditorWorld);
+ return true;
+ }
+
+ private void SyncScenesAndEntities(World world)
+ {
+ var sceneEntities = GroupEntitiesByScene(world);
+
+ foreach (var (scene, entities) in sceneEntities)
+ {
+ var sceneNode = FindOrCreateSceneNode(world, scene, _worldService.RootNodes);
+ SyncEntityTree(sceneNode, entities);
+ RemoveStaleEntityNodes(sceneNode, entities);
+ }
+
+ var activeScenes = new HashSet(sceneEntities.Keys);
+ RemoveStaleSceneNodes(_worldService.RootNodes, activeScenes);
+ }
+
+ private static SceneNode FindOrCreateSceneNode(World world, Scene scene, System.Collections.ObjectModel.ObservableCollection rootNodes)
+ {
+ foreach (var existing in rootNodes)
+ {
+ if (existing.Scene == scene)
+ {
+ return existing;
+ }
+ }
+
+ var sceneName = $"NewScene ({scene.ID})";
+ var newSceneNode = new SceneNode(world, scene, sceneName);
+ rootNodes.Add(newSceneNode);
+ return newSceneNode;
+ }
+
+ private void SyncEntityTree(SceneGraphNode parentNode, List entities)
+ {
+ var entitySet = new HashSet(entities);
+ var children = new Dictionary>();
+ var roots = new List();
+
+ foreach (var entity in entities)
+ {
+ Hierarchy hierarchy = default;
+ var hasHierarchy = TryGetHierarchyComponent(parentNode.World, entity, ref hierarchy);
+
+ if (hasHierarchy && hierarchy.parent.IsValid && entitySet.Contains(hierarchy.parent))
+ {
+ if (!children.TryGetValue(hierarchy.parent, out var list))
+ {
+ list = new List();
+ children[hierarchy.parent] = list;
+ }
+
+ list.Add(entity);
+ }
+ else
+ {
+ roots.Add(entity);
+ }
+ }
+
+ SyncExistingNodes(parentNode, roots, children);
+ }
+
+ private static void SyncExistingNodes(SceneGraphNode parentNode, List roots, Dictionary> children)
+ {
+ var existingNodeMap = new Dictionary();
+ for (var i = parentNode.Children.Count - 1; i >= 0; i--)
+ {
+ if (parentNode.Children[i] is EntityNode entityNode)
+ {
+ if (!roots.Contains(entityNode.Entity) && !children.ContainsKey(entityNode.Entity))
+ {
+ parentNode.Children.RemoveAt(i);
+ }
+ else
+ {
+ existingNodeMap[entityNode.Entity] = entityNode;
+ }
+ }
+ }
+
+ for (var i = 0; i < roots.Count; i++)
+ {
+ var entity = roots[i];
+ if (existingNodeMap.TryGetValue(entity, out var existingNode))
+ {
+ existingNodeMap.Remove(entity);
+
+ if (i >= parentNode.Children.Count || parentNode.Children[i] != existingNode)
+ {
+ parentNode.Children.Remove(existingNode);
+ parentNode.Children.Insert(i, existingNode);
+ }
+ }
+ else
+ {
+ var newNode = new EntityNode(parentNode.World, entity, "Entity");
+ parentNode.Children.Insert(i, newNode);
+ existingNode = newNode;
+ }
+
+ if (children.TryGetValue(entity, out var childList))
+ {
+ SyncExistingNodes(existingNode, childList, children);
+ }
+ else
+ {
+ for (var j = existingNode.Children.Count - 1; j >= 0; j--)
+ {
+ if (existingNode.Children[j] is EntityNode)
+ {
+ existingNode.Children.RemoveAt(j);
+ }
+ }
+ }
+ }
+ }
+
+ private static void RemoveStaleEntityNodes(SceneGraphNode parentNode, List entities)
+ {
+ var entitySet = new HashSet(entities);
+
+ for (var i = parentNode.Children.Count - 1; i >= 0; i--)
+ {
+ if (parentNode.Children[i] is EntityNode entityNode && !entitySet.Contains(entityNode.Entity))
+ {
+ parentNode.Children.RemoveAt(i);
+ }
+ }
+ }
+
+ private static void RemoveStaleSceneNodes(System.Collections.ObjectModel.ObservableCollection rootNodes, HashSet activeScenes)
+ {
+ for (var i = rootNodes.Count - 1; i >= 0; i--)
+ {
+ if (!activeScenes.Contains(rootNodes[i].Scene))
+ {
+ rootNodes.RemoveAt(i);
+ }
+ }
+ }
+
+ private static Dictionary> GroupEntitiesByScene(World world)
+ {
+ var sceneMap = new Dictionary>();
+ var queryID = new QueryBuilder().WithAll().Build(world);
+ ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
+
+ foreach (var chunk in query.GetChunkIterator())
+ {
+ var entities = chunk.GetEntities();
+ var sceneIDs = chunk.GetComponentData();
+
+ for (var i = 0; i < chunk.EntityCount; i++)
+ {
+ var s = sceneIDs[i].scene;
+ if (!s.IsValid)
+ {
+ continue;
+ }
+
+ if (!sceneMap.TryGetValue(s, out var list))
+ {
+ list = new List();
+ sceneMap[s] = list;
+ }
+
+ list.Add(entities[i]);
+ }
+ }
+
+ return sceneMap;
+ }
+
+ private static unsafe bool TryGetHierarchyComponent(World world, Entity entity, ref Hierarchy hierarchy)
+ {
+ var location = world.EntityManager.GetEntityLocation(entity);
+ if (!location.IsSuccess)
+ {
+ return false;
+ }
+
+ ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.Value.archetypeID);
+ var hierarchyID = ComponentTypeID.Value;
+ var pData = archetype.GetComponentData(location.Value.chunkIndex, location.Value.rowIndex, hierarchyID);
+ if (pData == null)
+ {
+ return false;
+ }
+
+ hierarchy = *(Hierarchy*)pData;
+ return true;
+ }
+}
diff --git a/src/Editor/Ghost.Editor/Ghost.Editor.csproj b/src/Editor/Ghost.Editor/Ghost.Editor.csproj
index f345500..be2a31b 100644
--- a/src/Editor/Ghost.Editor/Ghost.Editor.csproj
+++ b/src/Editor/Ghost.Editor/Ghost.Editor.csproj
@@ -38,10 +38,10 @@
-
+
-
-
+
+
diff --git a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml
index 56ed149..7b2ce3e 100644
--- a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml
+++ b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml
@@ -5,9 +5,40 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.Views.Controls"
+ xmlns:sg="using:Ghost.Editor.Core.SceneGraph"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -50,19 +81,17 @@
Margin="0,0,4,0"
FontSize="{StaticResource ToolbarFontIconFontSize}"
Glyph="" />
-
+
-
-
-
-
-
-
-
-
+
diff --git a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs
index b190fb5..4ecc32a 100644
--- a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs
+++ b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs
@@ -1,14 +1,62 @@
+using Ghost.Editor.Core.Contracts;
+using Ghost.Editor.Core.Services;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
-// To learn more about WinUI, the WinUI project structure,
-// and more about our project templates, see: http://aka.ms/winui-project-info.
-
namespace Ghost.Editor.Views.Controls;
public sealed partial class Hierarchy : UserControl
{
+ private readonly SceneGraphSyncService _syncService;
+ private readonly IInspectorService _inspectorService;
+ private readonly EditorWorldService _worldService;
+ private DispatcherQueueTimer? _syncTimer;
+
public Hierarchy()
{
InitializeComponent();
+
+ _syncService = App.GetService();
+ _inspectorService = App.GetService();
+ _worldService = App.GetService();
+
+ SceneTreeView.ItemsSource = _worldService.RootNodes;
+
+ SceneTreeView.ItemInvoked += OnTreeViewItemInvoked;
+ SceneTreeView.SelectionChanged += OnTreeViewSelectionChanged;
+
+ _syncTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
+ _syncTimer.Interval = TimeSpan.FromMilliseconds(100);
+ _syncTimer.Tick += OnSyncTick;
+ _syncTimer.Start();
+
+ Unloaded += OnUnloaded;
+ }
+
+ private void OnSyncTick(DispatcherQueueTimer sender, object args)
+ {
+ if (_syncService.Tick())
+ {
+ }
+ }
+
+ private void OnTreeViewItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
+ {
+ if (args.InvokedItem is IInspectable inspectable)
+ {
+ _inspectorService.SetSelected(inspectable, this);
+ }
+ }
+
+ private void OnTreeViewSelectionChanged(object sender, TreeViewSelectionChangedEventArgs args)
+ {
+ }
+
+ private void OnUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ _syncTimer?.Stop();
+ SceneTreeView.ItemInvoked -= OnTreeViewItemInvoked;
+ SceneTreeView.SelectionChanged -= OnTreeViewSelectionChanged;
+ Unloaded -= OnUnloaded;
}
}
diff --git a/src/Editor/Ghost.Editor/Views/Controls/SceneGraphTemplateSelector.cs b/src/Editor/Ghost.Editor/Views/Controls/SceneGraphTemplateSelector.cs
new file mode 100644
index 0000000..6950663
--- /dev/null
+++ b/src/Editor/Ghost.Editor/Views/Controls/SceneGraphTemplateSelector.cs
@@ -0,0 +1,35 @@
+using Ghost.Editor.Core.SceneGraph;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Ghost.Editor.Views.Controls;
+
+public partial class SceneGraphTemplateSelector : DataTemplateSelector
+{
+ public DataTemplate? SceneNodeTemplate
+ {
+ get; set;
+ }
+
+ public DataTemplate? EntityNodeTemplate
+ {
+ get; set;
+ }
+
+ protected override DataTemplate SelectTemplateCore(object item)
+ {
+ var result = item switch
+ {
+ SceneNode => SceneNodeTemplate,
+ EntityNode => EntityNodeTemplate,
+ _ => base.SelectTemplateCore(item)
+ };
+
+ return result!;
+ }
+
+ protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
+ {
+ return SelectTemplateCore(item);
+ }
+}
diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj
index 41ec652..ac8de7b 100644
--- a/src/Runtime/Ghost.Core/Ghost.Core.csproj
+++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj
@@ -11,6 +11,10 @@
True
+
+ $(DefineConstants);MHP_ENABLE_STACKTRACE
+
+
diff --git a/src/Runtime/Ghost.Engine/HierarchyUtility.cs b/src/Runtime/Ghost.Engine/HierarchyUtility.cs
new file mode 100644
index 0000000..f35d435
--- /dev/null
+++ b/src/Runtime/Ghost.Engine/HierarchyUtility.cs
@@ -0,0 +1,220 @@
+using Ghost.Core;
+using Ghost.Entities;
+using System.Runtime.CompilerServices;
+
+namespace Ghost.Engine;
+
+public static class HierarchyUtility
+{
+ public static Error SetParent(World world, Entity child, Entity parent)
+ {
+ if (!child.IsValid)
+ {
+ return Error.InvalidArgument;
+ }
+
+ if (!parent.IsValid)
+ {
+ return Error.InvalidArgument;
+ }
+
+ if (child == parent)
+ {
+ return Error.InvalidArgument;
+ }
+
+ if (!world.EntityManager.HasComponent(child))
+ {
+ return Error.NotFound;
+ }
+
+ if (!world.EntityManager.HasComponent(parent))
+ {
+ return Error.NotFound;
+ }
+
+ if (IsAncestor(world, parent, child))
+ {
+ return Error.InvalidArgument;
+ }
+
+ ref var childHierarchy = ref world.EntityManager.GetComponent(child);
+ if (Unsafe.IsNullRef(ref childHierarchy))
+ {
+ return Error.NotFound;
+ }
+
+ if (childHierarchy.parent.IsValid)
+ {
+ RemoveParent(world, child);
+ }
+
+ ref var parentHierarchy = ref world.EntityManager.GetComponent(parent);
+ if (Unsafe.IsNullRef(ref parentHierarchy))
+ {
+ return Error.NotFound;
+ }
+
+ childHierarchy.parent = parent;
+ childHierarchy.nextSibling = parentHierarchy.firstChild;
+ parentHierarchy.firstChild = child;
+
+ return Error.None;
+ }
+
+ public static Error RemoveParent(World world, Entity child)
+ {
+ if (!child.IsValid)
+ {
+ return Error.InvalidArgument;
+ }
+
+ if (!world.EntityManager.HasComponent(child))
+ {
+ return Error.NotFound;
+ }
+
+ ref var childHierarchy = ref world.EntityManager.GetComponent(child);
+ if (Unsafe.IsNullRef(ref childHierarchy))
+ {
+ return Error.NotFound;
+ }
+
+ var parent = childHierarchy.parent;
+ if (!parent.IsValid)
+ {
+ return Error.None;
+ }
+
+ ref var parentHierarchy = ref world.EntityManager.GetComponent(parent);
+ if (Unsafe.IsNullRef(ref parentHierarchy))
+ {
+ childHierarchy.parent = Entity.Invalid;
+ childHierarchy.nextSibling = Entity.Invalid;
+ return Error.None;
+ }
+
+ var prev = Entity.Invalid;
+ var current = parentHierarchy.firstChild;
+
+ while (current.IsValid)
+ {
+ ref var currentHierarchy = ref world.EntityManager.GetComponent(current);
+ if (Unsafe.IsNullRef(ref currentHierarchy))
+ {
+ break;
+ }
+
+ if (current == child)
+ {
+ if (prev.IsValid)
+ {
+ ref var prevHierarchy = ref world.EntityManager.GetComponent(prev);
+ prevHierarchy.nextSibling = childHierarchy.nextSibling;
+ }
+ else
+ {
+ parentHierarchy.firstChild = childHierarchy.nextSibling;
+ }
+
+ break;
+ }
+
+ prev = current;
+ current = currentHierarchy.nextSibling;
+ }
+
+ childHierarchy.parent = Entity.Invalid;
+ childHierarchy.nextSibling = Entity.Invalid;
+
+ return Error.None;
+ }
+
+ public static void DestroyEntityWithChildren(World world, Entity entity)
+ {
+ if (!entity.IsValid)
+ {
+ return;
+ }
+
+ if (world.EntityManager.HasComponent(entity))
+ {
+ ref var hierarchy = ref world.EntityManager.GetComponent(entity);
+ if (!Unsafe.IsNullRef(ref hierarchy))
+ {
+ var child = hierarchy.firstChild;
+ while (child.IsValid)
+ {
+ ref var childHierarchy = ref world.EntityManager.GetComponent(child);
+ var next = childHierarchy.nextSibling;
+ DestroyEntityWithChildren(world, child);
+ child = next;
+ }
+
+ RemoveParent(world, entity);
+ }
+ }
+
+ world.EntityManager.DestroyEntity(entity);
+ }
+
+ public static bool IsAncestor(World world, Entity entity, Entity potentialAncestor)
+ {
+ if (!entity.IsValid || !potentialAncestor.IsValid)
+ {
+ return false;
+ }
+
+ if (!world.EntityManager.HasComponent(entity))
+ {
+ return false;
+ }
+
+ ref var hierarchy = ref world.EntityManager.GetComponent(entity);
+ if (Unsafe.IsNullRef(ref hierarchy))
+ {
+ return false;
+ }
+
+ var current = hierarchy.parent;
+ while (current.IsValid)
+ {
+ if (current == potentialAncestor)
+ {
+ return true;
+ }
+
+ if (!world.EntityManager.HasComponent(current))
+ {
+ break;
+ }
+
+ ref var currentHierarchy = ref world.EntityManager.GetComponent(current);
+ if (Unsafe.IsNullRef(ref currentHierarchy))
+ {
+ break;
+ }
+
+ current = currentHierarchy.parent;
+ }
+
+ return false;
+ }
+
+ public static Error RemoveEntity(World world, Entity entity)
+ {
+ if (!entity.IsValid)
+ {
+ return Error.InvalidArgument;
+ }
+
+ if (world.EntityManager.HasComponent(entity))
+ {
+ RemoveParent(world, entity);
+ }
+
+ world.EntityManager.DestroyEntity(entity);
+
+ return Error.None;
+ }
+}
diff --git a/src/Runtime/Ghost.Entities/Archetype.cs b/src/Runtime/Ghost.Entities/Archetype.cs
index b5511a0..ccd9c67 100644
--- a/src/Runtime/Ghost.Entities/Archetype.cs
+++ b/src/Runtime/Ghost.Entities/Archetype.cs
@@ -125,11 +125,15 @@ internal unsafe struct Chunk : IDisposable
public Chunk(int bufferSize, int capacity, int componentCount, uint globalVersion)
{
_data = new UnsafeArray(bufferSize, AllocationHandle.Persistent, AllocationOption.Clear);
- _versions = new UnsafeArray(componentCount, AllocationHandle.Persistent);
_capacity = capacity;
_count = 0;
- _versions.AsSpan().Fill(globalVersion);
+ if (componentCount > 0)
+ {
+ _versions = new UnsafeArray(componentCount, AllocationHandle.Persistent);
+ _versions.AsSpan().Fill(globalVersion);
+ }
+
_structuralVersion = globalVersion;
}
diff --git a/src/Runtime/Ghost.Entities/System.cs b/src/Runtime/Ghost.Entities/System.cs
index c46740c..c592d25 100644
--- a/src/Runtime/Ghost.Entities/System.cs
+++ b/src/Runtime/Ghost.Entities/System.cs
@@ -332,6 +332,11 @@ public abstract class SystemGroup : ISystem
public void Initialize(ref readonly SystemAPI systemAPI)
{
+ if (_systems.Count == 0)
+ {
+ return;
+ }
+
ThrowIfNotSorted();
foreach (var system in _sortedSystems!)
@@ -342,6 +347,11 @@ public abstract class SystemGroup : ISystem
public void Update(ref readonly SystemAPI systemAPI)
{
+ if (_systems.Count == 0)
+ {
+ return;
+ }
+
ThrowIfNotSorted();
foreach (var system in _sortedSystems!)
@@ -352,6 +362,11 @@ public abstract class SystemGroup : ISystem
public void Cleanup(ref readonly SystemAPI systemAPI)
{
+ if (_systems.Count == 0)
+ {
+ return;
+ }
+
ThrowIfNotSorted();
foreach (var system in _sortedSystems!)
@@ -399,6 +414,11 @@ public sealed class SystemManager : IDisposable
internal void InitializeAll(TimeData timeData)
{
+ if (_systems.Count == 0)
+ {
+ return;
+ }
+
var systemAPI = new SystemAPI
{
Time = timeData,
@@ -413,6 +433,11 @@ public sealed class SystemManager : IDisposable
internal void UpdateAll(TimeData timeData)
{
+ if (_systems.Count == 0)
+ {
+ return;
+ }
+
var systemAPI = new SystemAPI
{
Time = timeData,
@@ -427,6 +452,11 @@ public sealed class SystemManager : IDisposable
internal void CleanupAll(TimeData timeData)
{
+ if (_systems.Count == 0)
+ {
+ return;
+ }
+
var systemAPI = new SystemAPI
{
Time = timeData,
diff --git a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj
index decb949..a5314dd 100644
--- a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj
+++ b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj
@@ -12,8 +12,7 @@
-
-
+
diff --git a/src/Test/Ghost.UnitTest/HierarchyUtilityTests.cs b/src/Test/Ghost.UnitTest/HierarchyUtilityTests.cs
new file mode 100644
index 0000000..e1de3ff
--- /dev/null
+++ b/src/Test/Ghost.UnitTest/HierarchyUtilityTests.cs
@@ -0,0 +1,286 @@
+using Ghost.Core;
+using Ghost.Engine;
+using Ghost.Engine.Components;
+using Ghost.Entities;
+using Misaki.HighPerformance.LowLevel.Buffer;
+
+namespace Ghost.UnitTest;
+
+[TestClass]
+[DoNotParallelize]
+public class HierarchyUtilityTests
+{
+ private World _world = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ AllocationManager.Initialize();
+ _world = World.Create(entityCapacity: 64);
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ World.Destroy(_world.ID);
+ AllocationManager.Dispose();
+ }
+
+ private Entity CreateHierarchyEntity()
+ {
+ var entity = _world.EntityManager.CreateEntity();
+ _world.EntityManager.AddComponent(entity, new Hierarchy
+ {
+ parent = Entity.Invalid,
+ firstChild = Entity.Invalid,
+ nextSibling = Entity.Invalid
+ });
+ return entity;
+ }
+
+ [TestMethod]
+ public void SetParent_ChildBecomesChildOfParent()
+ {
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ var result = HierarchyUtility.SetParent(_world, child, parent);
+
+ Assert.AreEqual(Error.None, result);
+
+ ref var childHierarchy = ref _world.EntityManager.GetComponent(child);
+ Assert.AreEqual(parent, childHierarchy.parent);
+
+ ref var parentHierarchy = ref _world.EntityManager.GetComponent(parent);
+ Assert.AreEqual(child, parentHierarchy.firstChild);
+ }
+
+ [TestMethod]
+ public void SetParent_SecondChildBecomesSibling()
+ {
+ var parent = CreateHierarchyEntity();
+ var child1 = CreateHierarchyEntity();
+ var child2 = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child1, parent);
+ HierarchyUtility.SetParent(_world, child2, parent);
+
+ ref var parentHierarchy = ref _world.EntityManager.GetComponent(parent);
+ Assert.AreEqual(child2, parentHierarchy.firstChild);
+
+ ref var child2Hierarchy = ref _world.EntityManager.GetComponent(child2);
+ Assert.AreEqual(child1, child2Hierarchy.nextSibling);
+
+ ref var child1Hierarchy = ref _world.EntityManager.GetComponent(child1);
+ Assert.AreEqual(Entity.Invalid, child1Hierarchy.nextSibling);
+ }
+
+ [TestMethod]
+ public void SetParent_ReparentFromOneParentToAnother()
+ {
+ var parent1 = CreateHierarchyEntity();
+ var parent2 = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child, parent1);
+ HierarchyUtility.SetParent(_world, child, parent2);
+
+ ref var childHierarchy = ref _world.EntityManager.GetComponent(child);
+ Assert.AreEqual(parent2, childHierarchy.parent);
+
+ ref var parent1Hierarchy = ref _world.EntityManager.GetComponent(parent1);
+ Assert.AreEqual(Entity.Invalid, parent1Hierarchy.firstChild);
+
+ ref var parent2Hierarchy = ref _world.EntityManager.GetComponent(parent2);
+ Assert.AreEqual(child, parent2Hierarchy.firstChild);
+ }
+
+ [TestMethod]
+ public void SetParent_SelfParentingIsRejected()
+ {
+ var entity = CreateHierarchyEntity();
+
+ var result = HierarchyUtility.SetParent(_world, entity, entity);
+
+ Assert.AreEqual(Error.InvalidArgument, result);
+ }
+
+ [TestMethod]
+ public void SetParent_CycleIsRejected()
+ {
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child, parent);
+
+ var result = HierarchyUtility.SetParent(_world, parent, child);
+
+ Assert.AreEqual(Error.InvalidArgument, result);
+ Assert.AreEqual(parent, _world.EntityManager.GetComponent(child).parent);
+ }
+
+ [TestMethod]
+ public void SetParent_GrandchildCycleIsRejected()
+ {
+ var grandparent = CreateHierarchyEntity();
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, parent, grandparent);
+ HierarchyUtility.SetParent(_world, child, parent);
+
+ var result = HierarchyUtility.SetParent(_world, grandparent, child);
+
+ Assert.AreEqual(Error.InvalidArgument, result);
+ }
+
+ [TestMethod]
+ public void SetParent_EntityWithoutHierarchyComponentReturnsNotFound()
+ {
+ var parent = CreateHierarchyEntity();
+ var child = _world.EntityManager.CreateEntity();
+
+ var result = HierarchyUtility.SetParent(_world, child, parent);
+
+ Assert.AreEqual(Error.NotFound, result);
+ }
+
+ [TestMethod]
+ public void RemoveParent_UnlinksChildFromParent()
+ {
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child, parent);
+ var result = HierarchyUtility.RemoveParent(_world, child);
+
+ Assert.AreEqual(Error.None, result);
+
+ ref var childHierarchy = ref _world.EntityManager.GetComponent(child);
+ Assert.AreEqual(Entity.Invalid, childHierarchy.parent);
+ Assert.AreEqual(Entity.Invalid, childHierarchy.nextSibling);
+
+ ref var parentHierarchy = ref _world.EntityManager.GetComponent(parent);
+ Assert.AreEqual(Entity.Invalid, parentHierarchy.firstChild);
+ }
+
+ [TestMethod]
+ public void RemoveParent_MiddleChildMaintainsSiblingChain()
+ {
+ var parent = CreateHierarchyEntity();
+ var child1 = CreateHierarchyEntity();
+ var child2 = CreateHierarchyEntity();
+ var child3 = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child1, parent);
+ HierarchyUtility.SetParent(_world, child2, parent);
+ HierarchyUtility.SetParent(_world, child3, parent);
+
+ HierarchyUtility.RemoveParent(_world, child2);
+
+ ref var child3Hierarchy = ref _world.EntityManager.GetComponent(child3);
+ Assert.AreEqual(child1, child3Hierarchy.nextSibling);
+
+ ref var child2Hierarchy = ref _world.EntityManager.GetComponent(child2);
+ Assert.AreEqual(Entity.Invalid, child2Hierarchy.parent);
+ Assert.AreEqual(Entity.Invalid, child2Hierarchy.nextSibling);
+ }
+
+ [TestMethod]
+ public void RemoveParent_WhenNoParentReturnsNone()
+ {
+ var entity = CreateHierarchyEntity();
+
+ var result = HierarchyUtility.RemoveParent(_world, entity);
+
+ Assert.AreEqual(Error.None, result);
+ }
+
+ [TestMethod]
+ public void IsAncestor_ReturnsTrueForDirectParent()
+ {
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child, parent);
+
+ Assert.IsTrue(HierarchyUtility.IsAncestor(_world, child, parent));
+ }
+
+ [TestMethod]
+ public void IsAncestor_ReturnsTrueForGrandparent()
+ {
+ var grandparent = CreateHierarchyEntity();
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, parent, grandparent);
+ HierarchyUtility.SetParent(_world, child, parent);
+
+ Assert.IsTrue(HierarchyUtility.IsAncestor(_world, child, grandparent));
+ }
+
+ [TestMethod]
+ public void IsAncestor_ReturnsFalseForUnrelatedEntity()
+ {
+ var entity1 = CreateHierarchyEntity();
+ var entity2 = CreateHierarchyEntity();
+
+ Assert.IsFalse(HierarchyUtility.IsAncestor(_world, entity1, entity2));
+ }
+
+ [TestMethod]
+ public void IsAncestor_ReturnsFalseForChild()
+ {
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child, parent);
+
+ Assert.IsFalse(HierarchyUtility.IsAncestor(_world, parent, child));
+ }
+
+ [TestMethod]
+ public void DestroyEntityWithChildren_CascadeDestroysAllChildren()
+ {
+ var parent = CreateHierarchyEntity();
+ var child1 = CreateHierarchyEntity();
+ var child2 = CreateHierarchyEntity();
+ var grandchild = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child1, parent);
+ HierarchyUtility.SetParent(_world, child2, parent);
+ HierarchyUtility.SetParent(_world, grandchild, child1);
+
+ HierarchyUtility.DestroyEntityWithChildren(_world, parent);
+
+ Assert.IsFalse(_world.EntityManager.Exists(parent));
+ Assert.IsFalse(_world.EntityManager.Exists(child1));
+ Assert.IsFalse(_world.EntityManager.Exists(child2));
+ Assert.IsFalse(_world.EntityManager.Exists(grandchild));
+ }
+
+ [TestMethod]
+ public void DestroyEntityWithChildren_DoesNotAffectUnrelatedEntities()
+ {
+ var parent = CreateHierarchyEntity();
+ var child = CreateHierarchyEntity();
+ var unrelated = CreateHierarchyEntity();
+
+ HierarchyUtility.SetParent(_world, child, parent);
+ HierarchyUtility.DestroyEntityWithChildren(_world, parent);
+
+ Assert.IsTrue(_world.EntityManager.Exists(unrelated));
+ }
+
+ [TestMethod]
+ public void RemoveEntity_DestroysSingleEntity()
+ {
+ var entity = CreateHierarchyEntity();
+
+ var result = HierarchyUtility.RemoveEntity(_world, entity);
+
+ Assert.AreEqual(Error.None, result);
+ Assert.IsFalse(_world.EntityManager.Exists(entity));
+ }
+}
diff --git a/src/Test/Ghost.UnitTest/SceneGraphBuilderTests.cs b/src/Test/Ghost.UnitTest/SceneGraphBuilderTests.cs
new file mode 100644
index 0000000..54c7865
--- /dev/null
+++ b/src/Test/Ghost.UnitTest/SceneGraphBuilderTests.cs
@@ -0,0 +1,174 @@
+using Ghost.Editor.Core.SceneGraph;
+using Ghost.Engine;
+using Ghost.Engine.Components;
+using Ghost.Engine.Core;
+using Ghost.Entities;
+using Misaki.HighPerformance.LowLevel.Buffer;
+
+namespace Ghost.UnitTest;
+
+[TestClass]
+[DoNotParallelize]
+public class SceneGraphBuilderTests
+{
+ private World _world = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ AllocationManager.Initialize();
+ _world = World.Create(entityCapacity: 64);
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ World.Destroy(_world.ID);
+ AllocationManager.Dispose();
+ }
+
+ private Entity CreateEntityWithScene(Scene scene)
+ {
+ var entity = _world.EntityManager.CreateEntity();
+ _world.EntityManager.AddComponent(entity, new SceneID { scene = scene });
+ return entity;
+ }
+
+ private Entity CreateEntityWithSceneAndHierarchy(Scene scene, Entity parent)
+ {
+ var entity = _world.EntityManager.CreateEntity();
+ _world.EntityManager.AddComponent(entity, new SceneID { scene = scene });
+ _world.EntityManager.AddComponent(entity, new Hierarchy
+ {
+ parent = Entity.Invalid,
+ firstChild = Entity.Invalid,
+ nextSibling = Entity.Invalid
+ });
+
+ if (parent.IsValid)
+ {
+ HierarchyUtility.SetParent(_world, entity, parent);
+ }
+
+ return entity;
+ }
+
+ [TestMethod]
+ public void Build_EmptyWorldReturnsEmptyList()
+ {
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ Assert.AreEqual(0, nodes.Count);
+ }
+
+ [TestMethod]
+ public void Build_OneSceneOneEntity_CreatesSceneNodeWithEntityChild()
+ {
+ var scene = SceneManager.CreateScene();
+ CreateEntityWithScene(scene);
+
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ Assert.AreEqual(1, nodes.Count);
+
+ var sceneNode = nodes[0];
+ Assert.AreEqual(scene, sceneNode.Scene);
+ Assert.AreEqual(1, sceneNode.Children.Count);
+ Assert.IsInstanceOfType(sceneNode.Children[0]);
+ }
+
+ [TestMethod]
+ public void Build_MultipleScenes_CreatesMultipleSceneNodes()
+ {
+ var scene1 = SceneManager.CreateScene();
+ var scene2 = SceneManager.CreateScene();
+ CreateEntityWithScene(scene1);
+ CreateEntityWithScene(scene2);
+
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ Assert.AreEqual(2, nodes.Count);
+ }
+
+ [TestMethod]
+ public void Build_HierarchicalEntities_CreatesNestedEntityNodes()
+ {
+ var scene = SceneManager.CreateScene();
+ var rootEntity = CreateEntityWithSceneAndHierarchy(scene, Entity.Invalid);
+ var childEntity = CreateEntityWithSceneAndHierarchy(scene, rootEntity);
+
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ Assert.AreEqual(1, nodes.Count);
+ var sceneNode = nodes[0];
+ Assert.AreEqual(1, sceneNode.Children.Count);
+
+ var rootNode = (EntityNode)sceneNode.Children[0];
+ Assert.AreEqual(rootEntity, rootNode.Entity);
+ Assert.AreEqual(1, rootNode.Children.Count);
+
+ var childNode = (EntityNode)rootNode.Children[0];
+ Assert.AreEqual(childEntity, childNode.Entity);
+ }
+
+ [TestMethod]
+ public void Build_EntitiesWithoutHierarchy_AreFlatChildren()
+ {
+ var scene = SceneManager.CreateScene();
+ CreateEntityWithScene(scene);
+ CreateEntityWithScene(scene);
+
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ Assert.AreEqual(1, nodes.Count);
+ var sceneNode = nodes[0];
+ Assert.AreEqual(2, sceneNode.Children.Count);
+ }
+
+ [TestMethod]
+ public void Build_SiblingOrder_PreservesChildOrder()
+ {
+ var scene = SceneManager.CreateScene();
+ var parent = CreateEntityWithSceneAndHierarchy(scene, Entity.Invalid);
+ var child1 = CreateEntityWithSceneAndHierarchy(scene, parent);
+ var child2 = CreateEntityWithSceneAndHierarchy(scene, parent);
+
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ var rootNode = (EntityNode)nodes[0].Children[0];
+ Assert.AreEqual(2, rootNode.Children.Count);
+ Assert.AreEqual(child2, ((EntityNode)rootNode.Children[0]).Entity);
+ Assert.AreEqual(child1, ((EntityNode)rootNode.Children[1]).Entity);
+ }
+
+ [TestMethod]
+ public void Build_DeepHierarchy_BuildsFullTree()
+ {
+ var scene = SceneManager.CreateScene();
+ var level1 = CreateEntityWithSceneAndHierarchy(scene, Entity.Invalid);
+ var level2 = CreateEntityWithSceneAndHierarchy(scene, level1);
+ var level3 = CreateEntityWithSceneAndHierarchy(scene, level2);
+
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ var n1 = (EntityNode)nodes[0].Children[0];
+ Assert.AreEqual(1, n1.Children.Count);
+
+ var n2 = (EntityNode)n1.Children[0];
+ Assert.AreEqual(1, n2.Children.Count);
+
+ var n3 = (EntityNode)n2.Children[0];
+ Assert.AreEqual(level3, n3.Entity);
+ }
+
+ [TestMethod]
+ public void Build_InvalidSceneEntitiesAreExcluded()
+ {
+ var entity = _world.EntityManager.CreateEntity();
+ _world.EntityManager.AddComponent(entity, new SceneID { scene = Scene.Invalid });
+
+ var nodes = SceneGraphBuilder.Build(_world);
+
+ Assert.AreEqual(0, nodes.Count);
+ }
+}