Compare commits
3 Commits
7dac1e4437
...
a1c5ccf937
| Author | SHA1 | Date | |
|---|---|---|---|
| a1c5ccf937 | |||
| 6b501efda0 | |||
| a40140cabd |
@@ -402,28 +402,28 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
|||||||
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
stream.Write(header);
|
stream.Write(header);
|
||||||
|
|
||||||
header.vertexOffset = (ulong)stream.Position;
|
header.vertexOffset = stream.Position;
|
||||||
await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token);
|
await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token);
|
||||||
|
|
||||||
header.indexOffset = (ulong)stream.Position;
|
header.indexOffset = stream.Position;
|
||||||
await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token);
|
await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token);
|
||||||
|
|
||||||
header.materialPartOffset = (ulong)stream.Position;
|
header.materialPartOffset = stream.Position;
|
||||||
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
|
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
|
||||||
|
|
||||||
header.meshletOffset = (ulong)stream.Position;
|
header.meshletOffset = stream.Position;
|
||||||
await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.GetRef().meshlets, token);
|
await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.GetRef().meshlets, token);
|
||||||
|
|
||||||
header.meshletGroupOffset = (ulong)stream.Position;
|
header.meshletGroupOffset = stream.Position;
|
||||||
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.GetRef().groups, token);
|
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.GetRef().groups, token);
|
||||||
|
|
||||||
header.meshletHierarchyNodeOffset = (ulong)stream.Position;
|
header.meshletHierarchyNodeOffset = stream.Position;
|
||||||
await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.GetRef().hierarchyNodes, token);
|
await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.GetRef().hierarchyNodes, token);
|
||||||
|
|
||||||
header.meshletVertexOffset = (ulong)stream.Position;
|
header.meshletVertexOffset = stream.Position;
|
||||||
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletVertices, token);
|
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletVertices, token);
|
||||||
|
|
||||||
header.meshletTriangleOffset = (ulong)stream.Position;
|
header.meshletTriangleOffset = stream.Position;
|
||||||
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletTriangles, token);
|
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletTriangles, token);
|
||||||
|
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
|
|||||||
@@ -5,35 +5,6 @@ namespace Ghost.Editor.Core;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DiscoverableAttributeBase : Attribute;
|
public abstract class DiscoverableAttributeBase : Attribute;
|
||||||
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
|
||||||
public class AssetOpenHandlerAttribute : DiscoverableAttributeBase
|
|
||||||
{
|
|
||||||
public string[] Extensions
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AssetOpenHandlerAttribute(params string[] extensions)
|
|
||||||
{
|
|
||||||
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
|
||||||
internal class AssetImporterAttribute : DiscoverableAttributeBase
|
|
||||||
{
|
|
||||||
public string[] SupportedExtensions
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AssetImporterAttribute(params string[] supportedExtensions)
|
|
||||||
{
|
|
||||||
SupportedExtensions = supportedExtensions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class)]
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
public class CustomEditorAttribute : DiscoverableAttributeBase
|
public class CustomEditorAttribute : DiscoverableAttributeBase
|
||||||
{
|
{
|
||||||
@@ -48,32 +19,6 @@ public class CustomEditorAttribute : DiscoverableAttributeBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
|
|
||||||
public class EditorInjectionAttribute : DiscoverableAttributeBase
|
|
||||||
{
|
|
||||||
public enum ServiceLifetime
|
|
||||||
{
|
|
||||||
Singleton,
|
|
||||||
Transient,
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServiceLifetime Lifetime
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type ImplementationType
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EditorInjectionAttribute(ServiceLifetime lifetime, Type implementationType)
|
|
||||||
{
|
|
||||||
Lifetime = lifetime;
|
|
||||||
ImplementationType = implementationType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase
|
public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Ghost.Core.Utilities;
|
using Ghost.Editor.Core.Utilities;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentIcons.WinUI" Version="2.1.326" />
|
<PackageReference Include="FluentIcons.WinUI" Version="2.1.326" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Resources;
|
|
||||||
|
|
||||||
public static class EditorIconSource
|
|
||||||
{
|
|
||||||
public static readonly IconSource scene_24 = new FontIconSource
|
|
||||||
{
|
|
||||||
Glyph = "\uF159",
|
|
||||||
FontSize = 24
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly IconSource entity_24 = new FontIconSource
|
|
||||||
{
|
|
||||||
Glyph = "\uF158",
|
|
||||||
FontSize = 24
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,521 +0,0 @@
|
|||||||
# 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<SceneGraphNode>`.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public abstract partial class SceneGraphNode : ObservableObject, IInspectable
|
|
||||||
{
|
|
||||||
public World World { get; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string Name { get; set; }
|
|
||||||
|
|
||||||
public ObservableCollection<SceneGraphNode> 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<SceneGraphNode>` (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<SceneGraphNode> 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 `<ListView>` with `<TreeView>`.
|
|
||||||
- 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
|
|
||||||
<TreeView ItemsSource="{x:Bind ViewModel.RootNodes, Mode=OneWay}">
|
|
||||||
<TreeView.ItemTemplateSelector>
|
|
||||||
<local:SceneGraphTemplateSelector />
|
|
||||||
</TreeView.ItemTemplateSelector>
|
|
||||||
</TreeView>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Move Templates to XAML Resources
|
|
||||||
|
|
||||||
Create a `ResourceDictionary` (or put in `App.xaml` / `Hierarchy.xaml.Resources`):
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<DataTemplate x:Key="SceneNodeTemplate" x:DataType="sg:SceneNode">
|
|
||||||
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
|
||||||
IsExpanded="True"
|
|
||||||
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<FontIcon FontSize="14" Glyph=""/>
|
|
||||||
<TextBlock Margin="10,0,0,0" Text="{x:Bind Name, Mode=OneWay}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</TreeViewItem>
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<DataTemplate x:Key="EntityNodeTemplate" x:DataType="sg:EntityNode">
|
|
||||||
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
|
||||||
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<FontIcon FontSize="14" Glyph=""/>
|
|
||||||
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</TreeViewItem>
|
|
||||||
</DataTemplate>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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"`. |
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# 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 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.
|
|
||||||
|
|
||||||
### 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 (JSON and 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.
|
|
||||||
|
|
||||||
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 (already installed) for binary serialization/deserialization because it supports both unmanaged and managed types.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using Ghost.Engine.Components;
|
using Ghost.Engine.Components;
|
||||||
using Ghost.Engine.Core;
|
using Ghost.Engine.Core;
|
||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
namespace Ghost.Editor.Core.SceneGraph;
|
||||||
|
|
||||||
public static class SceneGraphBuilder
|
public static class SceneGraphBuilder
|
||||||
{
|
{
|
||||||
public static List<SceneNode> Build(World world)
|
public static List<SceneNode> Build(World world, Dictionary<Entity, string>? initialNames = null)
|
||||||
{
|
{
|
||||||
var sceneNodes = new List<SceneNode>();
|
var sceneNodes = new List<SceneNode>();
|
||||||
var sceneEntities = GroupEntitiesByScene(world);
|
var sceneEntities = GroupEntitiesByScene(world);
|
||||||
@@ -15,7 +16,7 @@ public static class SceneGraphBuilder
|
|||||||
{
|
{
|
||||||
var sceneName = GetDefaultSceneName(scene);
|
var sceneName = GetDefaultSceneName(scene);
|
||||||
var sceneNode = new SceneNode(world, new Scene(scene), sceneName);
|
var sceneNode = new SceneNode(world, new Scene(scene), sceneName);
|
||||||
BuildEntityTree(entities, sceneNode);
|
BuildEntityTree(entities, sceneNode, initialNames);
|
||||||
sceneNodes.Add(sceneNode);
|
sceneNodes.Add(sceneNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,13 +34,13 @@ public static class SceneGraphBuilder
|
|||||||
var entities = chunk.GetEntities();
|
var entities = chunk.GetEntities();
|
||||||
var scene = chunk.GetSharedComponent<SceneID>();
|
var scene = chunk.GetSharedComponent<SceneID>();
|
||||||
|
|
||||||
for (var i = 0; i < chunk.EntityCount; i++)
|
|
||||||
{
|
|
||||||
if (scene.value == Scene.INVALID_ID)
|
if (scene.value == Scene.INVALID_ID)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < chunk.EntityCount; i++)
|
||||||
|
{
|
||||||
if (!sceneMap.TryGetValue(scene.value, out var list))
|
if (!sceneMap.TryGetValue(scene.value, out var list))
|
||||||
{
|
{
|
||||||
list = new List<Entity>();
|
list = new List<Entity>();
|
||||||
@@ -53,7 +54,7 @@ public static class SceneGraphBuilder
|
|||||||
return sceneMap;
|
return sceneMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BuildEntityTree(List<Entity> entities, SceneGraphNode parentNode)
|
private static void BuildEntityTree(List<Entity> entities, SceneGraphNode parentNode, Dictionary<Entity, string>? initialNames = null)
|
||||||
{
|
{
|
||||||
var entitySet = new HashSet<Entity>(entities);
|
var entitySet = new HashSet<Entity>(entities);
|
||||||
var childrenByParent = new Dictionary<Entity, List<Entity>>();
|
var childrenByParent = new Dictionary<Entity, List<Entity>>();
|
||||||
@@ -82,13 +83,14 @@ public static class SceneGraphBuilder
|
|||||||
|
|
||||||
foreach (var rootEntity in roots)
|
foreach (var rootEntity in roots)
|
||||||
{
|
{
|
||||||
var entityNode = new EntityNode(parentNode.World, rootEntity, "Entity");
|
var name = initialNames != null && initialNames.TryGetValue(rootEntity, out var n) ? n : "Entity";
|
||||||
|
var entityNode = new EntityNode(parentNode.World, rootEntity, name);
|
||||||
parentNode.Children.Add(entityNode);
|
parentNode.Children.Add(entityNode);
|
||||||
BuildSubtree(entityNode, childrenByParent);
|
BuildSubtree(entityNode, childrenByParent, initialNames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BuildSubtree(EntityNode parentNode, Dictionary<Entity, List<Entity>> childrenByParent)
|
private static void BuildSubtree(EntityNode parentNode, Dictionary<Entity, List<Entity>> childrenByParent, Dictionary<Entity, string>? initialNames = null)
|
||||||
{
|
{
|
||||||
if (!childrenByParent.TryGetValue(parentNode.Entity, out var childList))
|
if (!childrenByParent.TryGetValue(parentNode.Entity, out var childList))
|
||||||
{
|
{
|
||||||
@@ -100,9 +102,10 @@ public static class SceneGraphBuilder
|
|||||||
{
|
{
|
||||||
foreach (var childEntity in childList)
|
foreach (var childEntity in childList)
|
||||||
{
|
{
|
||||||
var childNode = new EntityNode(parentNode.World, childEntity, "Entity");
|
var name = initialNames != null && initialNames.TryGetValue(childEntity, out var n) ? n : "Entity";
|
||||||
|
var childNode = new EntityNode(parentNode.World, childEntity, name);
|
||||||
parentNode.Children.Add(childNode);
|
parentNode.Children.Add(childNode);
|
||||||
BuildSubtree(childNode, childrenByParent);
|
BuildSubtree(childNode, childrenByParent, initialNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -113,9 +116,10 @@ public static class SceneGraphBuilder
|
|||||||
{
|
{
|
||||||
if (childList.Contains(sibling))
|
if (childList.Contains(sibling))
|
||||||
{
|
{
|
||||||
var childNode = new EntityNode(parentNode.World, sibling, "Entity");
|
var name = initialNames != null && initialNames.TryGetValue(sibling, out var n) ? n : "Entity";
|
||||||
|
var childNode = new EntityNode(parentNode.World, sibling, name);
|
||||||
parentNode.Children.Add(childNode);
|
parentNode.Children.Add(childNode);
|
||||||
BuildSubtree(childNode, childrenByParent);
|
BuildSubtree(childNode, childrenByParent, initialNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
Hierarchy siblingHierarchy = default;
|
Hierarchy siblingHierarchy = default;
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
|
using Ghost.Core;
|
||||||
using Ghost.Editor.Core.SceneGraph;
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
|
using Ghost.Engine;
|
||||||
using Ghost.Engine.Core;
|
using Ghost.Engine.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Services;
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(EditorWorldService))]
|
|
||||||
public class EditorWorldService : IDisposable
|
public class EditorWorldService : IDisposable
|
||||||
{
|
{
|
||||||
private const int _DEFAULT_ENTITY_CAPACITY = 1024;
|
private const int DEFAULT_ENTITY_CAPACITY = 1024;
|
||||||
|
|
||||||
public World EditorWorld
|
public World EditorWorld
|
||||||
{
|
{
|
||||||
@@ -19,30 +22,209 @@ public class EditorWorldService : IDisposable
|
|||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
} = new();
|
} = new();
|
||||||
|
public event Action<Entity, string, ushort>? EntityCreated;
|
||||||
|
public event Action<Entity>? EntityDestroyed;
|
||||||
|
public event Action<Entity, Entity, Entity>? EntityParentChanged; // (child, oldParent, newParent)
|
||||||
|
public event Action<Entity, string>? EntityNameChanged;
|
||||||
|
public event Action? SceneGraphRebuilt;
|
||||||
|
|
||||||
public EditorWorldService()
|
public EditorWorldService()
|
||||||
{
|
{
|
||||||
EditorWorld = World.Create(entityCapacity: _DEFAULT_ENTITY_CAPACITY);
|
EditorWorld = World.Create(entityCapacity: DEFAULT_ENTITY_CAPACITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entity CreateEntity(string name, ushort sceneID, Entity parent = default)
|
||||||
|
{
|
||||||
|
var entity = EditorWorld.EntityManager.CreateEntity();
|
||||||
|
|
||||||
|
EditorWorld.EntityManager.AddComponent(entity, new Engine.Components.Hierarchy
|
||||||
|
{
|
||||||
|
parent = Entity.Invalid,
|
||||||
|
firstChild = Entity.Invalid,
|
||||||
|
nextSibling = Entity.Invalid
|
||||||
|
});
|
||||||
|
|
||||||
|
EditorWorld.EntityManager.AddSharedComponent(entity, new Engine.Components.SceneID
|
||||||
|
{
|
||||||
|
value = sceneID
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parent.IsValid)
|
||||||
|
{
|
||||||
|
HierarchyUtility.SetParent(EditorWorld, entity, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorWorld.AdvanceVersion();
|
||||||
|
EntityCreated?.Invoke(entity, name, sceneID);
|
||||||
|
|
||||||
|
if (parent.IsValid)
|
||||||
|
{
|
||||||
|
EntityParentChanged?.Invoke(entity, Entity.Invalid, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DestroyEntity(Entity entity)
|
||||||
|
{
|
||||||
|
if (!entity.IsValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DestroyEntityRecursive(entity);
|
||||||
|
EditorWorld.AdvanceVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DestroyEntityRecursive(Entity entity)
|
||||||
|
{
|
||||||
|
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(entity))
|
||||||
|
{
|
||||||
|
ref var hierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(entity);
|
||||||
|
var child = hierarchy.firstChild;
|
||||||
|
while (child.IsValid)
|
||||||
|
{
|
||||||
|
ref var childHierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child);
|
||||||
|
var next = childHierarchy.nextSibling;
|
||||||
|
DestroyEntityRecursive(child);
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HierarchyUtility.RemoveParent(EditorWorld, entity);
|
||||||
|
EditorWorld.EntityManager.DestroyEntity(entity);
|
||||||
|
EntityDestroyed?.Invoke(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSceneIDRecursive(Entity entity, ushort sceneID)
|
||||||
|
{
|
||||||
|
if (EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(entity))
|
||||||
|
{
|
||||||
|
EditorWorld.EntityManager.SetSharedComponent(entity, new Engine.Components.SceneID { value = sceneID });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(entity))
|
||||||
|
{
|
||||||
|
ref var hierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(entity);
|
||||||
|
var child = hierarchy.firstChild;
|
||||||
|
while (child.IsValid)
|
||||||
|
{
|
||||||
|
ref var childHierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child);
|
||||||
|
var next = childHierarchy.nextSibling;
|
||||||
|
UpdateSceneIDRecursive(child, sceneID);
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeEntityScene(Entity entity, ushort sceneID)
|
||||||
|
{
|
||||||
|
if (!entity.IsValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSceneIDRecursive(entity, sceneID);
|
||||||
|
EditorWorld.AdvanceVersion();
|
||||||
|
EntityParentChanged?.Invoke(entity, Entity.Invalid, Entity.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error SetParent(Entity child, Entity parent)
|
||||||
|
{
|
||||||
|
if (!child.IsValid)
|
||||||
|
{
|
||||||
|
return Error.InvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity oldParent = Entity.Invalid;
|
||||||
|
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(child))
|
||||||
|
{
|
||||||
|
oldParent = EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child).parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Error err;
|
||||||
|
if (parent.IsValid)
|
||||||
|
{
|
||||||
|
err = HierarchyUtility.SetParent(EditorWorld, child, parent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
err = HierarchyUtility.RemoveParent(EditorWorld, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err == Error.None)
|
||||||
|
{
|
||||||
|
if (parent.IsValid && EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(parent))
|
||||||
|
{
|
||||||
|
var locRes = EditorWorld.EntityManager.GetEntityLocation(parent);
|
||||||
|
if (locRes.IsSuccess)
|
||||||
|
{
|
||||||
|
ref var archetype = ref EditorWorld.ComponentManager.GetArchetypeReference(locRes.Value.archetypeID);
|
||||||
|
ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex);
|
||||||
|
var chunkView = new ChunkView(in archetype, in chunk);
|
||||||
|
var parentSceneID = chunkView.GetSharedComponent<Engine.Components.SceneID>().value;
|
||||||
|
UpdateSceneIDRecursive(child, parentSceneID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorWorld.AdvanceVersion();
|
||||||
|
EntityParentChanged?.Invoke(child, oldParent, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error RemoveParent(Entity child)
|
||||||
|
{
|
||||||
|
return SetParent(child, Entity.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ushort GetEntitySceneID(Entity entity)
|
||||||
|
{
|
||||||
|
if (!entity.IsValid)
|
||||||
|
{
|
||||||
|
return Scene.INVALID_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(entity))
|
||||||
|
{
|
||||||
|
var locRes = EditorWorld.EntityManager.GetEntityLocation(entity);
|
||||||
|
if (locRes.IsSuccess)
|
||||||
|
{
|
||||||
|
ref var archetype = ref EditorWorld.ComponentManager.GetArchetypeReference(locRes.Value.archetypeID);
|
||||||
|
ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex);
|
||||||
|
var chunkView = new ChunkView(in archetype, in chunk);
|
||||||
|
return chunkView.GetSharedComponent<Engine.Components.SceneID>().value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scene.INVALID_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RenameEntity(Entity entity, string newName)
|
||||||
|
{
|
||||||
|
if (!entity.IsValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityNameChanged?.Invoke(entity, newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateDefaultScene()
|
public void CreateDefaultScene()
|
||||||
{
|
{
|
||||||
var scene = SceneManager.CreateScene();
|
var scene = SceneManager.CreateScene();
|
||||||
var entity = EditorWorld.EntityManager.CreateEntity();
|
CreateEntity("Entity", scene.ID);
|
||||||
EditorWorld.EntityManager.AddSharedComponent(entity, new Engine.Components.SceneID
|
|
||||||
{
|
|
||||||
value = scene.ID
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
public void RebuildSceneGraph(Dictionary<Entity, string>? initialNames = null)
|
||||||
public void RebuildSceneGraph()
|
|
||||||
{
|
{
|
||||||
RootNodes.Clear();
|
RootNodes.Clear();
|
||||||
var sceneNodes = SceneGraphBuilder.Build(EditorWorld);
|
var sceneNodes = SceneGraphBuilder.Build(EditorWorld, initialNames);
|
||||||
foreach (var node in sceneNodes)
|
foreach (var node in sceneNodes)
|
||||||
{
|
{
|
||||||
RootNodes.Add(node);
|
RootNodes.Add(node);
|
||||||
}
|
}
|
||||||
|
SceneGraphRebuilt?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -1,53 +1,189 @@
|
|||||||
using Ghost.Editor.Core.SceneGraph;
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
using Ghost.Engine;
|
|
||||||
using Ghost.Engine.Components;
|
using Ghost.Engine.Components;
|
||||||
using Ghost.Engine.Core;
|
using Ghost.Engine.Core;
|
||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Services;
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(SceneGraphSyncService))]
|
public class SceneGraphSyncService : IDisposable
|
||||||
public class SceneGraphSyncService
|
|
||||||
{
|
{
|
||||||
private readonly EditorWorldService _worldService;
|
private readonly EditorWorldService _worldService;
|
||||||
private uint _lastSyncedVersion;
|
private readonly Dictionary<Entity, EntityNode> _nodeMap = new();
|
||||||
|
|
||||||
public SceneGraphSyncService(EditorWorldService worldService)
|
public SceneGraphSyncService(EditorWorldService worldService)
|
||||||
{
|
{
|
||||||
_worldService = worldService;
|
_worldService = worldService;
|
||||||
|
|
||||||
|
_worldService.EntityCreated += OnEntityCreated;
|
||||||
|
_worldService.EntityDestroyed += OnEntityDestroyed;
|
||||||
|
_worldService.EntityParentChanged += OnEntityParentChanged;
|
||||||
|
_worldService.EntityNameChanged += OnEntityNameChanged;
|
||||||
|
_worldService.SceneGraphRebuilt += OnSceneGraphRebuilt;
|
||||||
|
|
||||||
|
// Initialize node map from current root nodes
|
||||||
|
OnSceneGraphRebuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Tick()
|
public bool TryGetNode(Entity entity, out EntityNode node)
|
||||||
{
|
{
|
||||||
var currentVersion = _worldService.EditorWorld.Version;
|
return _nodeMap.TryGetValue(entity, out node!);
|
||||||
if (currentVersion == _lastSyncedVersion)
|
}
|
||||||
|
|
||||||
|
// Keep Tick as an empty stub returning false so we don't break Hierarchy.xaml.cs before we update it
|
||||||
|
public bool Tick()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastSyncedVersion = currentVersion;
|
public void Dispose()
|
||||||
SyncScenesAndEntities(_worldService.EditorWorld);
|
{
|
||||||
|
_worldService.EntityCreated -= OnEntityCreated;
|
||||||
|
_worldService.EntityDestroyed -= OnEntityDestroyed;
|
||||||
|
_worldService.EntityParentChanged -= OnEntityParentChanged;
|
||||||
|
_worldService.EntityNameChanged -= OnEntityNameChanged;
|
||||||
|
_worldService.SceneGraphRebuilt -= OnSceneGraphRebuilt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSceneGraphRebuilt()
|
||||||
|
{
|
||||||
|
_nodeMap.Clear();
|
||||||
|
foreach (var sceneNode in _worldService.RootNodes)
|
||||||
|
{
|
||||||
|
PopulateNodeMapRecursive(sceneNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateNodeMapRecursive(SceneGraphNode node)
|
||||||
|
{
|
||||||
|
if (node is EntityNode entityNode)
|
||||||
|
{
|
||||||
|
_nodeMap[entityNode.Entity] = entityNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var child in node.Children)
|
||||||
|
{
|
||||||
|
PopulateNodeMapRecursive(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEntityCreated(Entity entity, string name, ushort sceneID)
|
||||||
|
{
|
||||||
|
if (_nodeMap.ContainsKey(entity))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = new EntityNode(_worldService.EditorWorld, entity, name);
|
||||||
|
_nodeMap[entity] = node;
|
||||||
|
|
||||||
|
// By default, add to the scene's root collection
|
||||||
|
var sceneNode = FindOrCreateSceneNode(sceneID);
|
||||||
|
sceneNode.Children.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEntityDestroyed(Entity entity)
|
||||||
|
{
|
||||||
|
if (!_nodeMap.TryGetValue(entity, out var node))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively remove from node map
|
||||||
|
RemoveNodeAndDescendantsRecursive(node);
|
||||||
|
|
||||||
|
// Remove from its parent's Children collection (or from RootNodes if it was a scene's root entity)
|
||||||
|
RemoveNodeFromParent(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveNodeFromParent(EntityNode node)
|
||||||
|
{
|
||||||
|
foreach (var sceneNode in _worldService.RootNodes)
|
||||||
|
{
|
||||||
|
if (sceneNode.Children.Remove(node))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RemoveNodeFromChildrenRecursive(sceneNode.Children, node))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool RemoveNodeFromChildrenRecursive(System.Collections.ObjectModel.ObservableCollection<SceneGraphNode> children, EntityNode target)
|
||||||
|
{
|
||||||
|
foreach (var child in children)
|
||||||
|
{
|
||||||
|
if (child.Children.Remove(target))
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncScenesAndEntities(World world)
|
if (RemoveNodeFromChildrenRecursive(child.Children, target))
|
||||||
{
|
{
|
||||||
var sceneEntities = GroupEntitiesByScene(world);
|
return true;
|
||||||
|
}
|
||||||
foreach (var (scene, entities) in sceneEntities)
|
|
||||||
{
|
|
||||||
var sceneNode = FindOrCreateSceneNode(world, scene, _worldService.RootNodes);
|
|
||||||
SyncEntityTree(sceneNode, entities);
|
|
||||||
RemoveStaleEntityNodes(sceneNode, entities);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeScenes = new HashSet<ushort>(sceneEntities.Keys);
|
return false;
|
||||||
RemoveStaleSceneNodes(_worldService.RootNodes, activeScenes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SceneNode FindOrCreateSceneNode(World world, ushort sceneID, System.Collections.ObjectModel.ObservableCollection<SceneNode> rootNodes)
|
private void RemoveNodeAndDescendantsRecursive(EntityNode node)
|
||||||
{
|
{
|
||||||
foreach (var existing in rootNodes)
|
_nodeMap.Remove(node.Entity);
|
||||||
|
foreach (var child in node.Children)
|
||||||
|
{
|
||||||
|
if (child is EntityNode childEntityNode)
|
||||||
|
{
|
||||||
|
RemoveNodeAndDescendantsRecursive(childEntityNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEntityParentChanged(Entity child, Entity oldParent, Entity newParent)
|
||||||
|
{
|
||||||
|
if (!_nodeMap.TryGetValue(child, out var childNode))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from the old parent collection (wherever it currently is)
|
||||||
|
RemoveNodeFromParent(childNode);
|
||||||
|
|
||||||
|
// Add to the new parent collection (prepend at index 0 to match HierarchyUtility firstChild behavior)
|
||||||
|
if (newParent.IsValid && _nodeMap.TryGetValue(newParent, out var newParentNode))
|
||||||
|
{
|
||||||
|
newParentNode.Children.Insert(0, childNode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Add to the scene's root collection
|
||||||
|
if (_worldService.EditorWorld.EntityManager.HasComponent<SceneID>(child))
|
||||||
|
{
|
||||||
|
var sceneID = _worldService.GetEntitySceneID(child);
|
||||||
|
if (sceneID != Scene.INVALID_ID)
|
||||||
|
{
|
||||||
|
var sceneNode = FindOrCreateSceneNode(sceneID);
|
||||||
|
sceneNode.Children.Insert(0, childNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEntityNameChanged(Entity entity, string newName)
|
||||||
|
{
|
||||||
|
if (_nodeMap.TryGetValue(entity, out var node))
|
||||||
|
{
|
||||||
|
node.Name = newName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SceneNode FindOrCreateSceneNode(ushort sceneID)
|
||||||
|
{
|
||||||
|
foreach (var existing in _worldService.RootNodes)
|
||||||
{
|
{
|
||||||
if (existing.Scene.ID == sceneID)
|
if (existing.Scene.ID == sceneID)
|
||||||
{
|
{
|
||||||
@@ -56,168 +192,8 @@ public class SceneGraphSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sceneName = $"NewScene ({sceneID})";
|
var sceneName = $"NewScene ({sceneID})";
|
||||||
var newSceneNode = new SceneNode(world, new Scene(sceneID), sceneName);
|
var newSceneNode = new SceneNode(_worldService.EditorWorld, new Scene(sceneID), sceneName);
|
||||||
rootNodes.Add(newSceneNode);
|
_worldService.RootNodes.Add(newSceneNode);
|
||||||
return newSceneNode;
|
return newSceneNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncEntityTree(SceneGraphNode parentNode, List<Entity> entities)
|
|
||||||
{
|
|
||||||
var entitySet = new HashSet<Entity>(entities);
|
|
||||||
var children = new Dictionary<Entity, List<Entity>>();
|
|
||||||
var roots = new List<Entity>();
|
|
||||||
|
|
||||||
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<Entity>();
|
|
||||||
children[hierarchy.parent] = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(entity);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
roots.Add(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SyncExistingNodes(parentNode, roots, children);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SyncExistingNodes(SceneGraphNode parentNode, List<Entity> roots, Dictionary<Entity, List<Entity>> children)
|
|
||||||
{
|
|
||||||
var existingNodeMap = new Dictionary<Entity, EntityNode>();
|
|
||||||
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<Entity> entities)
|
|
||||||
{
|
|
||||||
var entitySet = new HashSet<Entity>(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<SceneNode> rootNodes, HashSet<ushort> activeScenes)
|
|
||||||
{
|
|
||||||
for (var i = rootNodes.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (!activeScenes.Contains(rootNodes[i].Scene.ID))
|
|
||||||
{
|
|
||||||
rootNodes.RemoveAt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Dictionary<ushort, List<Entity>> GroupEntitiesByScene(World world)
|
|
||||||
{
|
|
||||||
var sceneMap = new Dictionary<ushort, List<Entity>>();
|
|
||||||
var queryID = new QueryBuilder().WithAll<SceneID>().Build(world);
|
|
||||||
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
|
||||||
|
|
||||||
foreach (var chunk in query.GetChunkIterator())
|
|
||||||
{
|
|
||||||
var entities = chunk.GetEntities();
|
|
||||||
var scene = chunk.GetSharedComponent<SceneID>();
|
|
||||||
|
|
||||||
for (var i = 0; i < chunk.EntityCount; i++)
|
|
||||||
{
|
|
||||||
if (scene.value == Scene.INVALID_ID)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sceneMap.TryGetValue(scene.value, out var list))
|
|
||||||
{
|
|
||||||
list = new List<Entity>();
|
|
||||||
sceneMap[scene.value] = 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<Hierarchy>.Value;
|
|
||||||
var pData = archetype.GetComponentData(location.Value.chunkIndex, location.Value.rowIndex, hierarchyID);
|
|
||||||
if (pData == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hierarchy = *(Hierarchy*)pData;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,18 @@ internal sealed class SceneSaveData
|
|||||||
|
|
||||||
internal sealed class EntitySaveData
|
internal sealed class EntitySaveData
|
||||||
{
|
{
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = "Entity";
|
||||||
|
|
||||||
public Dictionary<string, JsonElement> Components
|
public Dictionary<string, JsonElement> Components
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
} = new();
|
} = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(SceneSerializationService))]
|
// TODO: Serialize shared components.
|
||||||
internal class SceneSerializationService : IDisposable
|
internal class SceneSerializationService : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<Type, FieldInfo[]> s_entityFieldsCache = new();
|
private static readonly Dictionary<Type, FieldInfo[]> s_entityFieldsCache = new();
|
||||||
@@ -66,11 +71,13 @@ internal class SceneSerializationService : IDisposable
|
|||||||
|
|
||||||
private readonly EditorWorldService _worldService;
|
private readonly EditorWorldService _worldService;
|
||||||
private readonly IAssetRegistry _assetRegistry;
|
private readonly IAssetRegistry _assetRegistry;
|
||||||
|
private readonly SceneGraphSyncService _syncService;
|
||||||
|
|
||||||
public SceneSerializationService(EditorWorldService worldService, IAssetRegistry assetRegistry)
|
public SceneSerializationService(EditorWorldService worldService, IAssetRegistry assetRegistry, SceneGraphSyncService syncService)
|
||||||
{
|
{
|
||||||
_worldService = worldService;
|
_worldService = worldService;
|
||||||
_assetRegistry = assetRegistry;
|
_assetRegistry = assetRegistry;
|
||||||
|
_syncService = syncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -84,11 +91,6 @@ internal class SceneSerializationService : IDisposable
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsEntityType(Type type)
|
|
||||||
{
|
|
||||||
return type == typeof(Entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static FieldInfo[] GetEntityFields(Type type)
|
private static FieldInfo[] GetEntityFields(Type type)
|
||||||
{
|
{
|
||||||
if (!s_entityFieldsCache.TryGetValue(type, out var fields))
|
if (!s_entityFieldsCache.TryGetValue(type, out var fields))
|
||||||
@@ -96,7 +98,7 @@ internal class SceneSerializationService : IDisposable
|
|||||||
var list = new List<FieldInfo>();
|
var list = new List<FieldInfo>();
|
||||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||||
{
|
{
|
||||||
if (IsEntityType(field.FieldType))
|
if (field.FieldType == typeof(Entity))
|
||||||
{
|
{
|
||||||
list.Add(field);
|
list.Add(field);
|
||||||
}
|
}
|
||||||
@@ -266,6 +268,11 @@ internal class SceneSerializationService : IDisposable
|
|||||||
{
|
{
|
||||||
var entityData = new EntitySaveData();
|
var entityData = new EntitySaveData();
|
||||||
|
|
||||||
|
if (entityElement.TryGetProperty("name", out var nameElement))
|
||||||
|
{
|
||||||
|
entityData.Name = nameElement.GetString() ?? "Entity";
|
||||||
|
}
|
||||||
|
|
||||||
if (entityElement.TryGetProperty("components", out var componentsElement))
|
if (entityElement.TryGetProperty("components", out var componentsElement))
|
||||||
{
|
{
|
||||||
foreach (var componentProperty in componentsElement.EnumerateObject())
|
foreach (var componentProperty in componentsElement.EnumerateObject())
|
||||||
@@ -296,13 +303,12 @@ internal class SceneSerializationService : IDisposable
|
|||||||
var activeScene = SceneManager.CreateScene();
|
var activeScene = SceneManager.CreateScene();
|
||||||
|
|
||||||
var entityCount = data.Entities.Count;
|
var entityCount = data.Entities.Count;
|
||||||
|
var forwardMap = new Dictionary<int, Entity>(entityCount);
|
||||||
if (entityCount == 0)
|
if (entityCount == 0)
|
||||||
{
|
{
|
||||||
goto RebuildAndReturn;
|
goto RebuildAndReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
var forwardMap = new Dictionary<int, Entity>(entityCount);
|
|
||||||
|
|
||||||
var scope = AllocationManager.CreateStackScope();
|
var scope = AllocationManager.CreateStackScope();
|
||||||
var typeIds = new UnsafeArray<UnsafeList<Identifier<IComponent>>>(entityCount, scope.AllocationHandle);
|
var typeIds = new UnsafeArray<UnsafeList<Identifier<IComponent>>>(entityCount, scope.AllocationHandle);
|
||||||
for (var i = 0; i < typeIds.Length; i++)
|
for (var i = 0; i < typeIds.Length; i++)
|
||||||
@@ -352,12 +358,21 @@ internal class SceneSerializationService : IDisposable
|
|||||||
world.EntityManager.SetSharedComponent(entity, new SceneID { value = activeScene.ID });
|
world.EntityManager.SetSharedComponent(entity, new SceneID { value = activeScene.ID });
|
||||||
|
|
||||||
var entityData = data.Entities[fileIndex];
|
var entityData = data.Entities[fileIndex];
|
||||||
ref var list = ref typeIds[fileIndex];
|
|
||||||
var idx = 1;
|
|
||||||
|
|
||||||
foreach (var (typeName, componentElement) in entityData.Components)
|
foreach (var (typeName, componentElement) in entityData.Components)
|
||||||
{
|
{
|
||||||
var compId = list[idx++];
|
var compId = ComponentRegistry.GetComponentIDByName(typeName);
|
||||||
|
if (compId.IsInvalid)
|
||||||
|
{
|
||||||
|
var type = TypeCache.GetTypes().FirstOrDefault(t => t.FullName == typeName);
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
compId = ComponentRegistry.GetComponentIDByName(typeName);
|
||||||
|
}
|
||||||
|
|
||||||
if (compId.IsInvalid)
|
if (compId.IsInvalid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -392,7 +407,15 @@ internal class SceneSerializationService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
RebuildAndReturn:
|
RebuildAndReturn:
|
||||||
_worldService.RebuildSceneGraph();
|
var initialNames = new Dictionary<Entity, string>();
|
||||||
|
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
|
||||||
|
{
|
||||||
|
if (forwardMap.TryGetValue(fileIndex, out var entity))
|
||||||
|
{
|
||||||
|
initialNames[entity] = data.Entities[fileIndex].Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_worldService.RebuildSceneGraph(initialNames);
|
||||||
return activeScene;
|
return activeScene;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +488,14 @@ internal class SceneSerializationService : IDisposable
|
|||||||
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
||||||
|
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
|
|
||||||
|
var entityName = "Entity";
|
||||||
|
if (_syncService != null && _syncService.TryGetNode(entity, out var node))
|
||||||
|
{
|
||||||
|
entityName = node.Name;
|
||||||
|
}
|
||||||
|
writer.WriteString("name", entityName);
|
||||||
|
|
||||||
writer.WriteStartObject("components");
|
writer.WriteStartObject("components");
|
||||||
|
|
||||||
foreach (var layout in archetype._layouts)
|
foreach (var layout in archetype._layouts)
|
||||||
|
|||||||
37
src/Editor/Ghost.Editor.Core/Utilities/PathUtility.cs
Normal file
37
src/Editor/Ghost.Editor.Core/Utilities/PathUtility.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Utilities;
|
||||||
|
|
||||||
|
public static class PathUtility
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static string Normalize(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static string GetUniqueName(string path)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(path);
|
||||||
|
directory ??= ".";
|
||||||
|
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||||
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
|
var uniqueName = fileName;
|
||||||
|
var counter = 1;
|
||||||
|
|
||||||
|
while (File.Exists(Path.Combine(directory, uniqueName + extension)))
|
||||||
|
{
|
||||||
|
uniqueName = $"{fileName} ({counter++})";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(directory, uniqueName + extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,12 +85,6 @@ public static class TypeCache
|
|||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Initialize()
|
|
||||||
{
|
|
||||||
// Intentionally left blank.
|
|
||||||
// This method exists to force the static constructor to run.
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Reload()
|
internal static void Reload()
|
||||||
{
|
{
|
||||||
s_types = LoadTypes();
|
s_types = LoadTypes();
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
TypeCache.Initialize();
|
|
||||||
|
|
||||||
Host = Microsoft.Extensions.Hosting.Host.
|
Host = Microsoft.Extensions.Hosting.Host.
|
||||||
CreateDefaultBuilder().
|
CreateDefaultBuilder().
|
||||||
UseContentRoot(AppContext.BaseDirectory).
|
UseContentRoot(AppContext.BaseDirectory).
|
||||||
@@ -67,38 +65,17 @@ public partial class App : Application
|
|||||||
services.AddSingleton<IInspectorService, InspectorService>();
|
services.AddSingleton<IInspectorService, InspectorService>();
|
||||||
services.AddSingleton<IPreviewService, PreviewService>();
|
services.AddSingleton<IPreviewService, PreviewService>();
|
||||||
services.AddSingleton<IAssetRegistry, AssetRegistry>();
|
services.AddSingleton<IAssetRegistry, AssetRegistry>();
|
||||||
|
|
||||||
|
services.AddSingleton<EditorWorldService>();
|
||||||
|
services.AddSingleton<SceneSerializationService>();
|
||||||
|
services.AddSingleton<SceneGraphSyncService>();
|
||||||
|
|
||||||
services.AddSingleton<IContentProvider, EditorContentProvider>();
|
services.AddSingleton<IContentProvider, EditorContentProvider>();
|
||||||
services.AddSingleton<IShaderCompilationBridge, EditorShaderCompilerBridge>();
|
services.AddSingleton<IShaderCompilationBridge, EditorShaderCompilerBridge>();
|
||||||
|
|
||||||
services.AddSingleton<EngineEditorViewModel>();
|
services.AddSingleton<EngineEditorViewModel>();
|
||||||
|
|
||||||
services.AddTransient<ContentBrowserViewModel>();
|
services.AddTransient<ContentBrowserViewModel>();
|
||||||
|
|
||||||
// TODO: Use source generators to generate this code at compile time instead of using reflection at runtime.
|
|
||||||
foreach (var type in TypeCache.GetTypes())
|
|
||||||
{
|
|
||||||
var data = type.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType == typeof(EditorInjectionAttribute));
|
|
||||||
if (data is null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lifeTime = (EditorInjectionAttribute.ServiceLifetime)data.ConstructorArguments[0].Value!;
|
|
||||||
var implementationType = (Type)data.ConstructorArguments[1].Value!;
|
|
||||||
var serviceType = type.IsInterface ? type.AsType() : implementationType;
|
|
||||||
|
|
||||||
switch (lifeTime)
|
|
||||||
{
|
|
||||||
case EditorInjectionAttribute.ServiceLifetime.Singleton:
|
|
||||||
services.AddSingleton(serviceType, implementationType);
|
|
||||||
break;
|
|
||||||
case EditorInjectionAttribute.ServiceLifetime.Transient:
|
|
||||||
services.AddTransient(serviceType, implementationType);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,10 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
|
||||||
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Ghost.Core.Utilities;
|
|
||||||
using Ghost.Editor.Core;
|
using Ghost.Editor.Core;
|
||||||
using Ghost.Editor.Core.Assets;
|
using Ghost.Editor.Core.Assets;
|
||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Ghost.Editor.Core.Utilities;
|
using Ghost.Editor.Core.Utilities;
|
||||||
using Ghost.Editor.Models;
|
using Ghost.Editor.Models;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
|
using Ghost.Engine.Streaming;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Ghost.Editor.Core;
|
using Ghost.Editor.Core;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
|
using Ghost.Editor.Core.Utilities;
|
||||||
|
using Ghost.Engine.Core;
|
||||||
|
|
||||||
namespace Ghost.Editor.Views.Controls;
|
namespace Ghost.Editor.Views.Controls;
|
||||||
|
|
||||||
@@ -54,5 +57,24 @@ internal partial class ContentBrowser
|
|||||||
[ContextMenuItem("project-browser", "Create/Asset/Scene")]
|
[ContextMenuItem("project-browser", "Create/Asset/Scene")]
|
||||||
private static void CreateSceneAsset()
|
private static void CreateSceneAsset()
|
||||||
{
|
{
|
||||||
|
var viewModel = LastFocused?.ViewModel;
|
||||||
|
if (viewModel is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentDir = viewModel.CurrentDirectoryPath;
|
||||||
|
if (!Directory.Exists(currentDir))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newScenePath = PathUtility.GetUniqueName(Path.Combine(currentDir, "New Scene.gscene"));
|
||||||
|
var tempScene = SceneManager.CreateScene();
|
||||||
|
|
||||||
|
var sceneSerializationService = App.GetService<SceneSerializationService>();
|
||||||
|
sceneSerializationService.SaveSceneFromEditorWorld(newScenePath, tempScene);
|
||||||
|
|
||||||
|
SceneManager.DestroyScene(tempScene, App.GetService<EditorWorldService>().EditorWorld);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +47,13 @@ internal sealed partial class ContentBrowser : UserControl
|
|||||||
|
|
||||||
private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e)
|
private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged;
|
//_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged;
|
||||||
GettingFocus += ProjectBrowser_GettingFocus;
|
GettingFocus += ProjectBrowser_GettingFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
|
private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
|
//_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
|
||||||
GettingFocus -= ProjectBrowser_GettingFocus;
|
GettingFocus -= ProjectBrowser_GettingFocus;
|
||||||
|
|
||||||
if (LastFocused == this)
|
if (LastFocused == this)
|
||||||
@@ -62,14 +62,14 @@ internal sealed partial class ContentBrowser : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _inspectorService_OnSelectionChanged(object? sender, InspectorSelectionChangedEventArgs e)
|
//private void _inspectorService_OnSelectionChanged(object? sender, InspectorSelectionChangedEventArgs e)
|
||||||
{
|
//{
|
||||||
if (e.Source is not ContentBrowserViewModel)
|
// if (e.Source is not ContentBrowserViewModel)
|
||||||
{
|
// {
|
||||||
PART_FilesView.DeselectAll();
|
// PART_FilesView.DeselectAll();
|
||||||
PART_DirectoriesView.SelectedNodes.Clear();
|
// PART_DirectoriesView.SelectedNodes.Clear();
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
private void PART_DirectoriesView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
|
private void PART_DirectoriesView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
@@ -80,7 +80,7 @@ internal sealed partial class ContentBrowser : UserControl
|
|||||||
|
|
||||||
_isUpdatingSelection = true;
|
_isUpdatingSelection = true;
|
||||||
|
|
||||||
PART_FilesView.DeselectAll();
|
//PART_FilesView.DeselectAll();
|
||||||
if (args.AddedItems.Count > 0 && args.AddedItems[0] is ExplorerItem selectedItem)
|
if (args.AddedItems.Count > 0 && args.AddedItems[0] is ExplorerItem selectedItem)
|
||||||
{
|
{
|
||||||
ViewModel.SelectedItem = selectedItem;
|
ViewModel.SelectedItem = selectedItem;
|
||||||
@@ -99,7 +99,7 @@ internal sealed partial class ContentBrowser : UserControl
|
|||||||
|
|
||||||
_isUpdatingSelection = true;
|
_isUpdatingSelection = true;
|
||||||
|
|
||||||
PART_DirectoriesView.SelectedNodes.Clear();
|
//PART_DirectoriesView.SelectedNodes.Clear();
|
||||||
if (PART_FilesView.SelectedItem is ExplorerItem selectedItem)
|
if (PART_FilesView.SelectedItem is ExplorerItem selectedItem)
|
||||||
{
|
{
|
||||||
ViewModel.SelectedItem = selectedItem;
|
ViewModel.SelectedItem = selectedItem;
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
||||||
IsExpanded="True"
|
IsExpanded="True"
|
||||||
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
||||||
|
<TreeViewItem.ContextFlyout>
|
||||||
|
<MenuFlyout>
|
||||||
|
<MenuFlyoutItem Text="Create Entity" Click="OnCreateEntityClick" />
|
||||||
|
</MenuFlyout>
|
||||||
|
</TreeViewItem.ContextFlyout>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<FontIcon FontSize="14" Glyph="" />
|
<FontIcon FontSize="14" Glyph="" />
|
||||||
<TextBlock Margin="10,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
|
<TextBlock Margin="10,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
|
||||||
@@ -31,6 +36,12 @@
|
|||||||
<TreeViewItem
|
<TreeViewItem
|
||||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
||||||
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
||||||
|
<TreeViewItem.ContextFlyout>
|
||||||
|
<MenuFlyout>
|
||||||
|
<MenuFlyoutItem Text="Create Child" Click="OnCreateChildClick" />
|
||||||
|
<MenuFlyoutItem Text="Delete" Click="OnDeleteEntityClick" />
|
||||||
|
</MenuFlyout>
|
||||||
|
</TreeViewItem.ContextFlyout>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<FontIcon FontSize="14" Glyph="" />
|
<FontIcon FontSize="14" Glyph="" />
|
||||||
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
|
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
|
||||||
@@ -92,6 +103,13 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Padding="4,2,0,2"
|
Padding="4,2,0,2"
|
||||||
ItemTemplateSelector="{StaticResource SceneGraphTemplateSelector}"
|
ItemTemplateSelector="{StaticResource SceneGraphTemplateSelector}"
|
||||||
SelectionMode="Single" />
|
SelectionMode="Single"
|
||||||
|
CanDragItems="True"
|
||||||
|
CanReorderItems="True"
|
||||||
|
AllowDrop="True"
|
||||||
|
DragItemsStarting="OnTreeViewDragItemsStarting"
|
||||||
|
DragOver="OnTreeViewDragOver"
|
||||||
|
Drop="OnTreeViewDrop"
|
||||||
|
KeyDown="OnTreeViewKeyDown" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Ghost.Editor.Core.Services;
|
using Ghost.Editor.Core.Services;
|
||||||
using Microsoft.UI.Dispatching;
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
|
using Ghost.Entities;
|
||||||
|
using Ghost.Engine;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Ghost.Editor.Views.Controls;
|
namespace Ghost.Editor.Views.Controls;
|
||||||
|
|
||||||
public sealed partial class Hierarchy : UserControl
|
public sealed partial class Hierarchy : UserControl
|
||||||
{
|
{
|
||||||
private readonly SceneGraphSyncService _syncService;
|
|
||||||
private readonly IInspectorService _inspectorService;
|
private readonly IInspectorService _inspectorService;
|
||||||
|
private readonly SceneGraphSyncService _syncService;
|
||||||
private readonly EditorWorldService _worldService;
|
private readonly EditorWorldService _worldService;
|
||||||
private DispatcherQueueTimer? _syncTimer;
|
private EntityNode? _draggedNode;
|
||||||
|
|
||||||
public Hierarchy()
|
public Hierarchy()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
_syncService = App.GetService<SceneGraphSyncService>();
|
|
||||||
_inspectorService = App.GetService<IInspectorService>();
|
_inspectorService = App.GetService<IInspectorService>();
|
||||||
|
_syncService = App.GetService<SceneGraphSyncService>();
|
||||||
_worldService = App.GetService<EditorWorldService>();
|
_worldService = App.GetService<EditorWorldService>();
|
||||||
|
|
||||||
SceneTreeView.ItemsSource = _worldService.RootNodes;
|
SceneTreeView.ItemsSource = _worldService.RootNodes;
|
||||||
@@ -25,21 +31,9 @@ public sealed partial class Hierarchy : UserControl
|
|||||||
SceneTreeView.ItemInvoked += OnTreeViewItemInvoked;
|
SceneTreeView.ItemInvoked += OnTreeViewItemInvoked;
|
||||||
SceneTreeView.SelectionChanged += OnTreeViewSelectionChanged;
|
SceneTreeView.SelectionChanged += OnTreeViewSelectionChanged;
|
||||||
|
|
||||||
_syncTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
|
||||||
_syncTimer.Interval = TimeSpan.FromMilliseconds(100);
|
|
||||||
_syncTimer.Tick += OnSyncTick;
|
|
||||||
_syncTimer.Start();
|
|
||||||
|
|
||||||
Unloaded += OnUnloaded;
|
Unloaded += OnUnloaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSyncTick(DispatcherQueueTimer sender, object args)
|
|
||||||
{
|
|
||||||
if (_syncService.Tick())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTreeViewItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
|
private void OnTreeViewItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
|
||||||
{
|
{
|
||||||
if (args.InvokedItem is IInspectable inspectable)
|
if (args.InvokedItem is IInspectable inspectable)
|
||||||
@@ -52,9 +46,150 @@ public sealed partial class Hierarchy : UserControl
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
private void OnTreeViewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == global::Windows.System.VirtualKey.Delete)
|
||||||
|
{
|
||||||
|
if (SceneTreeView.SelectedItem is EntityNode entityNode)
|
||||||
|
{
|
||||||
|
_worldService.DestroyEntity(entityNode.Entity);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTreeViewDragItemsStarting(TreeView sender, TreeViewDragItemsStartingEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.Items.Count > 0 && args.Items[0] is EntityNode entityNode)
|
||||||
|
{
|
||||||
|
_draggedNode = entityNode;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_draggedNode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTreeViewDragOver(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
|
||||||
|
|
||||||
|
if (_draggedNode == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetItem = GetAncestorTreeViewItem(e.OriginalSource as DependencyObject);
|
||||||
|
if (targetItem == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetNode = targetItem.DataContext as SceneGraphNode;
|
||||||
|
if (targetNode == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Can't drag onto itself
|
||||||
|
if (_draggedNode == targetNode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Can't drag onto a child of itself (cycle checking)
|
||||||
|
if (targetNode is EntityNode targetEntityNode)
|
||||||
|
{
|
||||||
|
if (HierarchyUtility.IsAncestor(_worldService.EditorWorld, targetEntityNode.Entity, _draggedNode.Entity))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTreeViewDrop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (_draggedNode == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetItem = GetAncestorTreeViewItem(e.OriginalSource as DependencyObject);
|
||||||
|
if (targetItem == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetNode = targetItem.DataContext as SceneGraphNode;
|
||||||
|
if (targetNode == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_draggedNode == targetNode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetNode is EntityNode targetEntityNode)
|
||||||
|
{
|
||||||
|
if (!HierarchyUtility.IsAncestor(_worldService.EditorWorld, targetEntityNode.Entity, _draggedNode.Entity))
|
||||||
|
{
|
||||||
|
_worldService.SetParent(_draggedNode.Entity, targetEntityNode.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (targetNode is SceneNode sceneNode)
|
||||||
|
{
|
||||||
|
_worldService.RemoveParent(_draggedNode.Entity);
|
||||||
|
_worldService.ChangeEntityScene(_draggedNode.Entity, sceneNode.Scene.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCreateEntityClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is SceneNode sceneNode)
|
||||||
|
{
|
||||||
|
_worldService.CreateEntity("Entity", sceneNode.Scene.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCreateChildClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is EntityNode entityNode)
|
||||||
|
{
|
||||||
|
var sceneID = _worldService.GetEntitySceneID(entityNode.Entity);
|
||||||
|
if (sceneID != Ghost.Engine.Core.Scene.INVALID_ID)
|
||||||
|
{
|
||||||
|
_worldService.CreateEntity("Entity", sceneID, parent: entityNode.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDeleteEntityClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is EntityNode entityNode)
|
||||||
|
{
|
||||||
|
_worldService.DestroyEntity(entityNode.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TreeViewItem? GetAncestorTreeViewItem(DependencyObject? current)
|
||||||
|
{
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (current is TreeViewItem item)
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
current = VisualTreeHelper.GetParent(current);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_syncTimer?.Stop();
|
|
||||||
SceneTreeView.ItemInvoked -= OnTreeViewItemInvoked;
|
SceneTreeView.ItemInvoked -= OnTreeViewItemInvoked;
|
||||||
SceneTreeView.SelectionChanged -= OnTreeViewSelectionChanged;
|
SceneTreeView.SelectionChanged -= OnTreeViewSelectionChanged;
|
||||||
Unloaded -= OnUnloaded;
|
Unloaded -= OnUnloaded;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Ghost.Core.Utilities;
|
|
||||||
|
|
||||||
public static class PathUtility
|
|
||||||
{
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static string Normalize(string? path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -66,7 +67,8 @@ public static class StreamUtility
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static MemoryBlock ReadMemory(this Stream stream, long length, AllocationHandle allocationHandle)
|
public static MemoryBlock ReadMemory(this Stream stream, long length, AllocationHandle allocationHandle)
|
||||||
{
|
{
|
||||||
var memory = new MemoryBlock((nuint)length, 16, allocationHandle);
|
var alignedLength = MemoryUtility.AlignUp((nuint)length, 16);
|
||||||
|
var memory = new MemoryBlock(alignedLength, 16, allocationHandle);
|
||||||
|
|
||||||
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
|
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
|
||||||
var maxChunkSize = (int)Math.Min(0x7fffffffL, length);
|
var maxChunkSize = (int)Math.Min(0x7fffffffL, length);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Ghost.Engine.Streaming;
|
|||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Ghost.Engine.Core;
|
namespace Ghost.Engine.Core;
|
||||||
@@ -106,15 +107,418 @@ public class LoadedSceneData : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum SceneLoadStatus
|
||||||
|
{
|
||||||
|
Queued = 0,
|
||||||
|
WaitingForDependencies = 1,
|
||||||
|
Parsing = 2,
|
||||||
|
WaitingForMaterialization = 3,
|
||||||
|
Materializing = 4,
|
||||||
|
Completed = 5,
|
||||||
|
Failed = 6,
|
||||||
|
Canceled = 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SceneLoadOptions
|
||||||
|
{
|
||||||
|
public bool DeferMaterialization;
|
||||||
|
public int MaxEntitiesPerFrame;
|
||||||
|
public int Priority;
|
||||||
|
|
||||||
|
public readonly bool AutoMaterialize => !DeferMaterialization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct SceneMaterializeBudget
|
||||||
|
{
|
||||||
|
public readonly int MaxScenes;
|
||||||
|
public readonly int MaxEntities;
|
||||||
|
|
||||||
|
public SceneMaterializeBudget(int maxEntities, int maxScenes = 0)
|
||||||
|
{
|
||||||
|
MaxEntities = maxEntities;
|
||||||
|
MaxScenes = maxScenes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SceneMaterializeBudget Unlimited => new(int.MaxValue, int.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SceneLoadOperation
|
||||||
|
{
|
||||||
|
private readonly PendingSceneLoad _pendingLoad;
|
||||||
|
|
||||||
|
internal PendingSceneLoad PendingLoad => _pendingLoad;
|
||||||
|
|
||||||
|
public SceneLoadStatus Status => _pendingLoad.Status;
|
||||||
|
public float Progress => _pendingLoad.Progress;
|
||||||
|
public Scene Scene => _pendingLoad.Scene;
|
||||||
|
public string? ErrorMessage => _pendingLoad.ErrorMessage;
|
||||||
|
public bool IsParsed => _pendingLoad.Status >= SceneLoadStatus.WaitingForMaterialization && _pendingLoad.Status < SceneLoadStatus.Failed;
|
||||||
|
public bool IsMaterialized => _pendingLoad.Status == SceneLoadStatus.Completed;
|
||||||
|
public bool IsCompleted => _pendingLoad.Status is SceneLoadStatus.Completed or SceneLoadStatus.Failed or SceneLoadStatus.Canceled;
|
||||||
|
|
||||||
|
internal SceneLoadOperation(PendingSceneLoad pendingLoad)
|
||||||
|
{
|
||||||
|
_pendingLoad = pendingLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
_pendingLoad.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PendingSceneLoad : IDisposable
|
||||||
|
{
|
||||||
|
private enum MaterializePhase
|
||||||
|
{
|
||||||
|
NotStarted = 0,
|
||||||
|
CreateEntities = 1,
|
||||||
|
SetComponents = 2,
|
||||||
|
RemapEntityReferences = 3,
|
||||||
|
Completed = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly AssetEntry _sceneEntry;
|
||||||
|
private readonly SceneLoadOptions _options;
|
||||||
|
|
||||||
|
private LoadedSceneData? _loadedData;
|
||||||
|
private UnsafeArray<Entity> _fileLocalToRuntimeEntity;
|
||||||
|
private MaterializePhase _phase;
|
||||||
|
private int _nextCreateIndex;
|
||||||
|
private int _nextSetComponentIndex;
|
||||||
|
private int _nextRemapIndex;
|
||||||
|
private int _status;
|
||||||
|
private int _releasedSceneEntry;
|
||||||
|
private bool _singleResetApplied;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public World World { get; }
|
||||||
|
public AssetRef<Scene> SceneAsset { get; }
|
||||||
|
public SceneLoadingType LoadingType { get; }
|
||||||
|
public Scene Scene { get; private set; }
|
||||||
|
public SceneLoadOptions Options => _options;
|
||||||
|
public SceneLoadStatus Status => (SceneLoadStatus)Volatile.Read(ref _status);
|
||||||
|
public string? ErrorMessage { get; private set; }
|
||||||
|
public bool IsTerminal => Status is SceneLoadStatus.Completed or SceneLoadStatus.Failed or SceneLoadStatus.Canceled;
|
||||||
|
|
||||||
|
public float Progress
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var status = Status;
|
||||||
|
if (status == SceneLoadStatus.Completed)
|
||||||
|
{
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadedData = _loadedData;
|
||||||
|
if (loadedData == null || !loadedData.entities.IsCreated || loadedData.entities.Length == 0)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
SceneLoadStatus.Queued => 0.0f,
|
||||||
|
SceneLoadStatus.WaitingForDependencies => 0.1f,
|
||||||
|
SceneLoadStatus.Parsing => 0.25f,
|
||||||
|
SceneLoadStatus.WaitingForMaterialization => 0.5f,
|
||||||
|
SceneLoadStatus.Materializing => 0.75f,
|
||||||
|
_ => 0.0f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityCount = loadedData.entities.Length;
|
||||||
|
var completedEntities = Math.Min(entityCount * 3, _nextCreateIndex + _nextSetComponentIndex + _nextRemapIndex);
|
||||||
|
return 0.5f + (0.5f * completedEntities / (entityCount * 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PendingSceneLoad(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType, SceneLoadOptions options, AssetEntry sceneEntry)
|
||||||
|
{
|
||||||
|
World = world;
|
||||||
|
SceneAsset = sceneAsset;
|
||||||
|
LoadingType = loadingType;
|
||||||
|
_options = options;
|
||||||
|
_sceneEntry = sceneEntry;
|
||||||
|
Scene = Scene.Invalid;
|
||||||
|
_status = (int)SceneLoadStatus.Queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStatus(SceneLoadStatus status)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _status, (int)status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteParsing(LoadedSceneData loadedData)
|
||||||
|
{
|
||||||
|
_loadedData = loadedData;
|
||||||
|
SetStatus(SceneLoadStatus.WaitingForMaterialization);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fail(string message)
|
||||||
|
{
|
||||||
|
ErrorMessage = message;
|
||||||
|
SetStatus(SceneLoadStatus.Failed);
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
var status = Status;
|
||||||
|
if (status is SceneLoadStatus.Completed or SceneLoadStatus.Failed or SceneLoadStatus.Canceled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatus(SceneLoadStatus.Canceled);
|
||||||
|
if (status >= SceneLoadStatus.WaitingForMaterialization)
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Materialize(int maxEntities)
|
||||||
|
{
|
||||||
|
if (maxEntities <= 0 || IsTerminal)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadedData = _loadedData;
|
||||||
|
if (loadedData == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_singleResetApplied && LoadingType == SceneLoadingType.Single)
|
||||||
|
{
|
||||||
|
SceneManager.ReleaseMaterializedSceneReferences(World);
|
||||||
|
World.Reset();
|
||||||
|
_singleResetApplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_phase == MaterializePhase.NotStarted)
|
||||||
|
{
|
||||||
|
Scene = SceneManager.CreateScene();
|
||||||
|
_fileLocalToRuntimeEntity = new UnsafeArray<Entity>(loadedData.entities.Length, AllocationHandle.Persistent);
|
||||||
|
_phase = MaterializePhase.CreateEntities;
|
||||||
|
SetStatus(SceneLoadStatus.Materializing);
|
||||||
|
}
|
||||||
|
|
||||||
|
var consumed = 0;
|
||||||
|
while (consumed < maxEntities && _phase != MaterializePhase.Completed)
|
||||||
|
{
|
||||||
|
switch (_phase)
|
||||||
|
{
|
||||||
|
case MaterializePhase.CreateEntities:
|
||||||
|
consumed += MaterializeCreateEntities(loadedData, maxEntities - consumed);
|
||||||
|
if (_nextCreateIndex >= loadedData.entities.Length)
|
||||||
|
{
|
||||||
|
_phase = MaterializePhase.SetComponents;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MaterializePhase.SetComponents:
|
||||||
|
consumed += MaterializeSetComponents(loadedData, maxEntities - consumed);
|
||||||
|
if (_nextSetComponentIndex >= loadedData.entities.Length)
|
||||||
|
{
|
||||||
|
_phase = MaterializePhase.RemapEntityReferences;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MaterializePhase.RemapEntityReferences:
|
||||||
|
consumed += MaterializeRemapEntityReferences(loadedData, maxEntities - consumed);
|
||||||
|
if (_nextRemapIndex >= loadedData.entities.Length)
|
||||||
|
{
|
||||||
|
CompleteMaterialization();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int MaterializeCreateEntities(LoadedSceneData loadedData, int maxEntities)
|
||||||
|
{
|
||||||
|
using var scope = AllocationManager.CreateStackScope();
|
||||||
|
using var sharedCom = new SharedComponentSet(256, scope.AllocationHandle);
|
||||||
|
|
||||||
|
var consumed = 0;
|
||||||
|
while (consumed < maxEntities && _nextCreateIndex < loadedData.entities.Length)
|
||||||
|
{
|
||||||
|
ref var pending = ref loadedData.entities[_nextCreateIndex];
|
||||||
|
|
||||||
|
using var typeIds = new UnsafeList<Identifier<IComponent>>(pending.componentTypeIDs.Count + 1, scope.AllocationHandle);
|
||||||
|
typeIds.Add(ComponentTypeID<SceneID>.Value);
|
||||||
|
for (var i = 0; i < pending.componentTypeIDs.Count; i++)
|
||||||
|
{
|
||||||
|
typeIds.Add(pending.componentTypeIDs[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedCom.With(new SceneID { value = Scene.ID });
|
||||||
|
|
||||||
|
var set = new ComponentSetView(typeIds, sharedCom);
|
||||||
|
var entity = World.EntityManager.CreateEntity(set);
|
||||||
|
_fileLocalToRuntimeEntity[pending.fileLocalIndex] = entity;
|
||||||
|
|
||||||
|
sharedCom.Reset();
|
||||||
|
_nextCreateIndex++;
|
||||||
|
consumed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe int MaterializeSetComponents(LoadedSceneData loadedData, int maxEntities)
|
||||||
|
{
|
||||||
|
var consumed = 0;
|
||||||
|
while (consumed < maxEntities && _nextSetComponentIndex < loadedData.entities.Length)
|
||||||
|
{
|
||||||
|
ref var pending = ref loadedData.entities[_nextSetComponentIndex];
|
||||||
|
var entity = _fileLocalToRuntimeEntity[pending.fileLocalIndex];
|
||||||
|
if (entity.IsValid)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < pending.componentData.Count; i++)
|
||||||
|
{
|
||||||
|
var (typeID, data) = pending.componentData[i];
|
||||||
|
World.EntityManager.SetComponent(entity, typeID, data.GetUnsafePtr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextSetComponentIndex++;
|
||||||
|
consumed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe int MaterializeRemapEntityReferences(LoadedSceneData loadedData, int maxEntities)
|
||||||
|
{
|
||||||
|
var consumed = 0;
|
||||||
|
while (consumed < maxEntities && _nextRemapIndex < loadedData.entities.Length)
|
||||||
|
{
|
||||||
|
ref var pending = ref loadedData.entities[_nextRemapIndex];
|
||||||
|
var entity = _fileLocalToRuntimeEntity[pending.fileLocalIndex];
|
||||||
|
if (entity.IsValid)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < pending.entityFields.Count; i++)
|
||||||
|
{
|
||||||
|
var (componentDataIndex, fieldOffsets) = pending.entityFields[i];
|
||||||
|
var compTypeID = pending.componentData[componentDataIndex].typeID;
|
||||||
|
|
||||||
|
var pComponent = World.EntityManager.GetComponent(entity, compTypeID);
|
||||||
|
if (pComponent == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var f = 0; f < fieldOffsets.Length; f++)
|
||||||
|
{
|
||||||
|
var fieldOffset = fieldOffsets[f];
|
||||||
|
var pField = (byte*)pComponent + fieldOffset;
|
||||||
|
var fileLocalIndex = *(int*)pField;
|
||||||
|
var remappedEntity = fileLocalIndex >= 0 && fileLocalIndex < _fileLocalToRuntimeEntity.Length ?
|
||||||
|
_fileLocalToRuntimeEntity[fileLocalIndex] :
|
||||||
|
Entity.Invalid;
|
||||||
|
|
||||||
|
*(Entity*)pField = remappedEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextRemapIndex++;
|
||||||
|
consumed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteMaterialization()
|
||||||
|
{
|
||||||
|
_phase = MaterializePhase.Completed;
|
||||||
|
SceneManager.RegisterMaterializedScene(this);
|
||||||
|
|
||||||
|
_loadedData?.Dispose();
|
||||||
|
_loadedData = null;
|
||||||
|
|
||||||
|
if (_fileLocalToRuntimeEntity.IsCreated)
|
||||||
|
{
|
||||||
|
_fileLocalToRuntimeEntity.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatus(SceneLoadStatus.Completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseMaterializedReference()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _releasedSceneEntry, 1) == 0)
|
||||||
|
{
|
||||||
|
_sceneEntry.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadedData?.Dispose();
|
||||||
|
_loadedData = null;
|
||||||
|
|
||||||
|
if (_fileLocalToRuntimeEntity.IsCreated)
|
||||||
|
{
|
||||||
|
_fileLocalToRuntimeEntity.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Status != SceneLoadStatus.Completed)
|
||||||
|
{
|
||||||
|
ReleaseMaterializedReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages scenes within a world.
|
/// Manages scenes within a world.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SceneManager
|
public static class SceneManager
|
||||||
{
|
{
|
||||||
|
private readonly struct SceneKey : IEquatable<SceneKey>
|
||||||
|
{
|
||||||
|
private readonly Identifier<World> _worldID;
|
||||||
|
private readonly ushort _sceneID;
|
||||||
|
|
||||||
|
public SceneKey(World world, Scene scene)
|
||||||
|
{
|
||||||
|
_worldID = world.ID;
|
||||||
|
_sceneID = scene.ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(SceneKey other)
|
||||||
|
{
|
||||||
|
return _worldID == other._worldID && _sceneID == other._sceneID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return obj is SceneKey other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(_worldID, _sceneID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ushort s_nextSceneID;
|
private static ushort s_nextSceneID;
|
||||||
private static readonly Queue<ushort> s_recycledSceneIDs = new();
|
private static readonly Queue<ushort> s_recycledSceneIDs = new();
|
||||||
|
|
||||||
private static readonly Lock s_creationLock = new();
|
private static readonly Lock s_creationLock = new();
|
||||||
|
private static readonly Lock s_loadedScenesLock = new();
|
||||||
|
private static readonly ConcurrentQueue<PendingSceneLoad> s_pendingMaterialization = new();
|
||||||
|
private static readonly Dictionary<SceneKey, PendingSceneLoad> s_loadedScenes = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new scene in the world.
|
/// Creates a new scene in the world.
|
||||||
@@ -244,6 +648,77 @@ public static class SceneManager
|
|||||||
return ParseSceneData(header, ref reader, allocationHandle);
|
return ParseSceneData(header, ref reader, allocationHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void EnqueuePendingScene(PendingSceneLoad pendingSceneLoad)
|
||||||
|
{
|
||||||
|
s_pendingMaterialization.Enqueue(pendingSceneLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void RegisterMaterializedScene(PendingSceneLoad pendingSceneLoad)
|
||||||
|
{
|
||||||
|
lock (s_loadedScenesLock)
|
||||||
|
{
|
||||||
|
s_loadedScenes[new SceneKey(pendingSceneLoad.World, pendingSceneLoad.Scene)] = pendingSceneLoad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ReleaseMaterializedSceneReferences(World world)
|
||||||
|
{
|
||||||
|
lock (s_loadedScenesLock)
|
||||||
|
{
|
||||||
|
foreach (var (key, pendingLoad) in s_loadedScenes.ToArray())
|
||||||
|
{
|
||||||
|
if (pendingLoad.World != world)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingLoad.ReleaseMaterializedReference();
|
||||||
|
s_recycledSceneIDs.Enqueue(pendingLoad.Scene.ID);
|
||||||
|
s_loadedScenes.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MaterializePendingScenes(World world, SceneMaterializeBudget budget = default)
|
||||||
|
{
|
||||||
|
var maxScenes = budget.MaxScenes > 0 ? budget.MaxScenes : int.MaxValue;
|
||||||
|
var remainingEntities = budget.MaxEntities > 0 ? budget.MaxEntities : int.MaxValue;
|
||||||
|
var pendingCount = s_pendingMaterialization.Count;
|
||||||
|
var processedScenes = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < pendingCount && processedScenes < maxScenes && remainingEntities > 0; i++)
|
||||||
|
{
|
||||||
|
if (!s_pendingMaterialization.TryDequeue(out var pendingLoad))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingLoad.World != world || pendingLoad.IsTerminal)
|
||||||
|
{
|
||||||
|
if (!pendingLoad.IsTerminal)
|
||||||
|
{
|
||||||
|
s_pendingMaterialization.Enqueue(pendingLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sceneBudget = pendingLoad.Options.MaxEntitiesPerFrame > 0 ?
|
||||||
|
Math.Min(remainingEntities, pendingLoad.Options.MaxEntitiesPerFrame) :
|
||||||
|
remainingEntities;
|
||||||
|
|
||||||
|
var consumed = pendingLoad.Materialize(sceneBudget);
|
||||||
|
remainingEntities -= consumed;
|
||||||
|
|
||||||
|
if (!pendingLoad.IsTerminal)
|
||||||
|
{
|
||||||
|
s_pendingMaterialization.Enqueue(pendingLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
processedScenes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Materializes the loaded scene data into actual entities in the world, setting their components and remapping entity references.
|
/// Materializes the loaded scene data into actual entities in the world, setting their components and remapping entity references.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -274,7 +749,10 @@ public static class SceneManager
|
|||||||
|
|
||||||
using var typeIds = new UnsafeList<Identifier<IComponent>>(pending.componentTypeIDs.Count + 1, scope.AllocationHandle);
|
using var typeIds = new UnsafeList<Identifier<IComponent>>(pending.componentTypeIDs.Count + 1, scope.AllocationHandle);
|
||||||
typeIds.Add(ComponentTypeID<SceneID>.Value);
|
typeIds.Add(ComponentTypeID<SceneID>.Value);
|
||||||
|
if (pending.componentTypeIDs.Count > 0)
|
||||||
|
{
|
||||||
typeIds.AddRange(pending.componentTypeIDs);
|
typeIds.AddRange(pending.componentTypeIDs);
|
||||||
|
}
|
||||||
|
|
||||||
sharedCom.With(new SceneID { value = scene.ID });
|
sharedCom.With(new SceneID { value = scene.ID });
|
||||||
|
|
||||||
@@ -340,9 +818,9 @@ public static class SceneManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Destroys all entities belonging to the specified scene.
|
/// Destroys all entities belonging to the specified scene.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scene">The scene to unload.</param>
|
/// <param name="scene">The scene to destroy.</param>
|
||||||
/// <param name="world">The world containing the entities.</param>
|
/// <param name="world">The world containing the entities.</param>
|
||||||
public static void UnloadScene(Scene scene, World world)
|
public static void DestroyScene(Scene scene, World world)
|
||||||
{
|
{
|
||||||
var queryID = new QueryBuilder().WithAll<SceneID>().Build(world);
|
var queryID = new QueryBuilder().WithAll<SceneID>().Build(world);
|
||||||
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
||||||
@@ -358,6 +836,15 @@ public static class SceneManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
s_recycledSceneIDs.Enqueue(scene.ID);
|
s_recycledSceneIDs.Enqueue(scene.ID);
|
||||||
|
|
||||||
|
lock (s_loadedScenesLock)
|
||||||
|
{
|
||||||
|
var key = new SceneKey(world, scene);
|
||||||
|
if (s_loadedScenes.Remove(key, out var pendingLoad))
|
||||||
|
{
|
||||||
|
pendingLoad.ReleaseMaterializedReference();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using Ghost.Graphics.RHI;
|
|||||||
using Ghost.Graphics.Services;
|
using Ghost.Graphics.Services;
|
||||||
using Misaki.HighPerformance.Jobs;
|
using Misaki.HighPerformance.Jobs;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using TerraFX.Interop.Windows;
|
|
||||||
|
|
||||||
namespace Ghost.Engine.Streaming;
|
namespace Ghost.Engine.Streaming;
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ internal abstract class AssetEntry : IAssetEntry
|
|||||||
private int _refCount;
|
private int _refCount;
|
||||||
private int _state;
|
private int _state;
|
||||||
|
|
||||||
private bool _pendingReimport;
|
private int _pendingReimport;
|
||||||
|
|
||||||
protected ResourceManager ResourceManager => _resourceManager;
|
protected ResourceManager ResourceManager => _resourceManager;
|
||||||
protected IResourceDatabase ResourceDatabase => _resourceDatabase;
|
protected IResourceDatabase ResourceDatabase => _resourceDatabase;
|
||||||
@@ -88,7 +87,7 @@ internal abstract class AssetEntry : IAssetEntry
|
|||||||
Volatile.Write(ref _state, (int)value);
|
Volatile.Write(ref _state, (int)value);
|
||||||
if (Volatile.Read(ref _state) == (int)AssetState.Ready)
|
if (Volatile.Read(ref _state) == (int)AssetState.Ready)
|
||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref _pendingReimport, false, true))
|
if (Interlocked.Exchange(ref _pendingReimport, 0) == 1)
|
||||||
{
|
{
|
||||||
_assetManager.ReimportAsset(_assetId); // re-queue
|
_assetManager.ReimportAsset(_assetId); // re-queue
|
||||||
}
|
}
|
||||||
@@ -116,7 +115,7 @@ internal abstract class AssetEntry : IAssetEntry
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void SetPendingReimport()
|
public void SetPendingReimport()
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _pendingReimport, true);
|
Volatile.Write(ref _pendingReimport, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -155,7 +154,7 @@ internal abstract class AssetEntry : IAssetEntry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAssetEntry
|
internal interface IAssetEntry
|
||||||
{
|
{
|
||||||
Guid AssetId { get; }
|
Guid AssetId { get; }
|
||||||
AssetType AssetType { get; }
|
AssetType AssetType { get; }
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ internal struct LoadAssetJob : IJob
|
|||||||
|
|
||||||
entry.State = AssetState.Loading;
|
entry.State = AssetState.Loading;
|
||||||
|
|
||||||
|
if (entry is not ILoadableAssetEntry loadable)
|
||||||
|
{
|
||||||
|
entry.State = AssetState.Loaded;
|
||||||
|
if (!assetManager.StreamingProcessor.EnqueueForProcess(entry))
|
||||||
|
{
|
||||||
|
entry.State = AssetState.Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var openResult = assetManager.ContentProvider.OpenRead(entry.AssetId);
|
var openResult = assetManager.ContentProvider.OpenRead(entry.AssetId);
|
||||||
@@ -53,18 +64,13 @@ internal struct LoadAssetJob : IJob
|
|||||||
}
|
}
|
||||||
|
|
||||||
using var stream = openResult.Value;
|
using var stream = openResult.Value;
|
||||||
|
|
||||||
if (entry is ILoadableAssetEntry loadable)
|
|
||||||
{
|
|
||||||
var result = loadable.OnLoadContent(stream);
|
var result = loadable.OnLoadContent(stream);
|
||||||
|
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
{
|
{
|
||||||
entry.State = AssetState.Failed;
|
entry.State = AssetState.Failed;
|
||||||
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
|
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
entry.State = AssetState.Loaded;
|
entry.State = AssetState.Loaded;
|
||||||
if (!assetManager.StreamingProcessor.EnqueueForProcess(entry))
|
if (!assetManager.StreamingProcessor.EnqueueForProcess(entry))
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ internal struct MeshContentHeader
|
|||||||
public float3 boundsMin;
|
public float3 boundsMin;
|
||||||
public float3 boundsMax;
|
public float3 boundsMax;
|
||||||
|
|
||||||
public ulong vertexOffset;
|
public long vertexOffset;
|
||||||
public ulong indexOffset;
|
public long indexOffset;
|
||||||
public ulong materialPartOffset;
|
public long materialPartOffset;
|
||||||
public ulong meshletOffset;
|
public long meshletOffset;
|
||||||
public ulong meshletGroupOffset;
|
public long meshletGroupOffset;
|
||||||
public ulong meshletHierarchyNodeOffset;
|
public long meshletHierarchyNodeOffset;
|
||||||
public ulong meshletVertexOffset;
|
public long meshletVertexOffset;
|
||||||
public ulong meshletTriangleOffset;
|
public long meshletTriangleOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
@@ -122,16 +122,20 @@ internal unsafe class MeshAssetEntry : AssetEntry, ILoadableAssetEntry, IUploada
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override void OnReleaseResource()
|
public override void OnReleaseResource()
|
||||||
|
{
|
||||||
|
ResourceManager.ReleaseMesh(_actualHandle);
|
||||||
|
if (_tempHandle.IsValid)
|
||||||
{
|
{
|
||||||
ResourceManager.ReleaseMesh(_tempHandle);
|
ResourceManager.ReleaseMesh(_tempHandle);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Result OnLoadContent(Stream contentStream)
|
public Result OnLoadContent(Stream contentStream)
|
||||||
{
|
{
|
||||||
bool ValidateRange(ulong offset, int count, uint stride)
|
bool ValidateRange(long offset, int count, uint stride)
|
||||||
{
|
{
|
||||||
var size = (ulong)count * stride;
|
var size = count * stride;
|
||||||
return offset <= _rawData.Size && size <= _rawData.Size - (nuint)offset;
|
return offset <= contentStream.Length && size <= contentStream.Length - offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
var header = contentStream.Read<MeshContentHeader>();
|
var header = contentStream.Read<MeshContentHeader>();
|
||||||
@@ -330,6 +334,7 @@ internal unsafe class MeshAssetEntry : AssetEntry, ILoadableAssetEntry, IUploada
|
|||||||
dstMesh.MeshletTrianglesBuffer = context.ResourceDatabase.Replace(temp.MeshletTrianglesBuffer.AsResource(), srcMesh.MeshletTrianglesBuffer.AsResource()).AsBuffer();
|
dstMesh.MeshletTrianglesBuffer = context.ResourceDatabase.Replace(temp.MeshletTrianglesBuffer.AsResource(), srcMesh.MeshletTrianglesBuffer.AsResource()).AsBuffer();
|
||||||
|
|
||||||
context.ResourceManager.ReleaseMesh(_tempHandle);
|
context.ResourceManager.ReleaseMesh(_tempHandle);
|
||||||
|
_tempHandle = Handle<Mesh>.Invalid;
|
||||||
|
|
||||||
context.CommandBuffer.Barrier(
|
context.CommandBuffer.Barrier(
|
||||||
BarrierDesc.Buffer(dstMesh.VertexBuffer, BarrierSync.VertexShading, BarrierAccess.VertexBuffer | BarrierAccess.ShaderResource),
|
BarrierDesc.Buffer(dstMesh.VertexBuffer, BarrierSync.VertexShading, BarrierAccess.VertexBuffer | BarrierAccess.ShaderResource),
|
||||||
@@ -341,8 +346,6 @@ internal unsafe class MeshAssetEntry : AssetEntry, ILoadableAssetEntry, IUploada
|
|||||||
BarrierDesc.Buffer(dstMesh.MeshletHierarchyBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource),
|
BarrierDesc.Buffer(dstMesh.MeshletHierarchyBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource),
|
||||||
BarrierDesc.Buffer(dstMesh.MeshDataBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource));
|
BarrierDesc.Buffer(dstMesh.MeshDataBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource));
|
||||||
|
|
||||||
_actualHandle = Handle<Mesh>.Invalid;
|
|
||||||
|
|
||||||
_rawData.Dispose();
|
_rawData.Dispose();
|
||||||
_pVertices = null;
|
_pVertices = null;
|
||||||
_pIndices = null;
|
_pIndices = null;
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
|
|||||||
{
|
{
|
||||||
while (_pendingFinalize.TryDequeue(out var item))
|
while (_pendingFinalize.TryDequeue(out var item))
|
||||||
{
|
{
|
||||||
item.State = AssetState.Ready;
|
|
||||||
item.OnUploadComplete(context);
|
item.OnUploadComplete(context);
|
||||||
|
item.State = AssetState.Ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingCopyFenceValue = 0;
|
_pendingCopyFenceValue = 0;
|
||||||
|
|||||||
@@ -29,24 +29,43 @@ public partial class AssetManager
|
|||||||
public SceneContentHeader header;
|
public SceneContentHeader header;
|
||||||
public Stream stream;
|
public Stream stream;
|
||||||
|
|
||||||
public LoadedSceneData loadedSceneData;
|
public PendingSceneLoad pendingSceneLoad;
|
||||||
|
|
||||||
public readonly void Execute(ref readonly JobExecutionContext context)
|
public readonly void Execute(ref readonly JobExecutionContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent);
|
if (pendingSceneLoad.Status == SceneLoadStatus.Canceled)
|
||||||
if (loadResult.IsFailure)
|
|
||||||
{
|
{
|
||||||
Logger.Error($"Failed to parse scene data: {loadResult.Message}");
|
pendingSceneLoad.Dispose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedSceneData.entities = loadResult.Value.entities;
|
pendingSceneLoad.SetStatus(SceneLoadStatus.Parsing);
|
||||||
|
|
||||||
|
var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent);
|
||||||
|
if (loadResult.IsFailure)
|
||||||
|
{
|
||||||
|
pendingSceneLoad.Fail(loadResult.Message ?? "Failed to parse scene data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingSceneLoad.Status == SceneLoadStatus.Canceled)
|
||||||
|
{
|
||||||
|
loadResult.Value.Dispose();
|
||||||
|
pendingSceneLoad.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSceneLoad.CompleteParsing(loadResult.Value);
|
||||||
|
if (pendingSceneLoad.Options.AutoMaterialize)
|
||||||
|
{
|
||||||
|
SceneManager.EnqueuePendingScene(pendingSceneLoad);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error($"Exception while loading scene: {ex}");
|
pendingSceneLoad.Fail(ex.Message);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -55,7 +74,7 @@ public partial class AssetManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe Result<JobHandle> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType, ref LoadedSceneData? loadedSceneData)
|
public unsafe Result<SceneLoadOperation> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType, SceneLoadOptions options = default)
|
||||||
{
|
{
|
||||||
if (!sceneAsset.IsValid)
|
if (!sceneAsset.IsValid)
|
||||||
{
|
{
|
||||||
@@ -91,23 +110,19 @@ public partial class AssetManager
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (loadingType == SceneLoadingType.Single)
|
|
||||||
{
|
|
||||||
world.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedSceneData ??= new LoadedSceneData();
|
|
||||||
|
|
||||||
var entry = GetOrCreateEntry(sceneAsset.ID); // Purely to get the dependencies and ensure the asset is tracked, the actual loading is done in the job.
|
var entry = GetOrCreateEntry(sceneAsset.ID); // Purely to get the dependencies and ensure the asset is tracked, the actual loading is done in the job.
|
||||||
|
var pendingSceneLoad = new PendingSceneLoad(world, sceneAsset, loadingType, options, entry);
|
||||||
|
pendingSceneLoad.SetStatus(SceneLoadStatus.WaitingForDependencies);
|
||||||
|
|
||||||
var job = new LoadSceneJob
|
var job = new LoadSceneJob
|
||||||
{
|
{
|
||||||
header = header,
|
header = header,
|
||||||
stream = stream,
|
stream = stream,
|
||||||
loadedSceneData = loadedSceneData
|
pendingSceneLoad = pendingSceneLoad
|
||||||
};
|
};
|
||||||
|
|
||||||
return _jobScheduler.Schedule(in job, entry.LoadJobHandle);
|
_jobScheduler.Schedule(in job, entry.LoadJobHandle);
|
||||||
|
return Result<SceneLoadOperation>.Success(new SceneLoadOperation(pendingSceneLoad));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -103,9 +103,13 @@ internal unsafe class TextureAssetEntry : AssetEntry, ILoadableAssetEntry, IUplo
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override void OnReleaseResource()
|
public override void OnReleaseResource()
|
||||||
|
{
|
||||||
|
ResourceDatabase.ReleaseResource(_actualHandle.AsResource());
|
||||||
|
if (_tempHandle.IsValid)
|
||||||
{
|
{
|
||||||
ResourceDatabase.ReleaseResource(_tempHandle.AsResource());
|
ResourceDatabase.ReleaseResource(_tempHandle.AsResource());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Result OnLoadContent(Stream contentStream)
|
public Result OnLoadContent(Stream contentStream)
|
||||||
{
|
{
|
||||||
@@ -167,8 +171,8 @@ internal unsafe class TextureAssetEntry : AssetEntry, ILoadableAssetEntry, IUplo
|
|||||||
|
|
||||||
context.CommandBuffer.Barrier(BarrierDesc.Texture(actualHandle, BarrierSync.AllShading, BarrierAccess.ShaderResource, BarrierLayout.ShaderResource));
|
context.CommandBuffer.Barrier(BarrierDesc.Texture(actualHandle, BarrierSync.AllShading, BarrierAccess.ShaderResource, BarrierLayout.ShaderResource));
|
||||||
|
|
||||||
_actualHandle = Handle<GPUTexture>.Invalid;
|
_actualHandle = actualHandle.AsTexture();
|
||||||
_tempHandle = actualHandle.AsTexture();
|
_tempHandle = Handle<GPUTexture>.Invalid;
|
||||||
_textureData.Dispose();
|
_textureData.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -636,19 +636,74 @@ public unsafe partial struct EntityQuery : IDisposable
|
|||||||
|
|
||||||
if (!requiresFiltering)
|
if (!requiresFiltering)
|
||||||
{
|
{
|
||||||
// No enablement constraints? Any entity in this chunk is a match!
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var k = 0; k < chunk._count; k++)
|
var ulongCount = (chunk._count + 63) / 64;
|
||||||
|
var chunkHasMatch = false;
|
||||||
|
|
||||||
|
// Loop through 64 entities at a time
|
||||||
|
for (var block = 0; block < ulongCount; block++)
|
||||||
{
|
{
|
||||||
if (IsEntityValid(chunk.GetUnsafePtr(), k, in archetype, in _mask))
|
// Start assuming all 64 entities in this block are valid (ulong.MaxValue = 1111...)
|
||||||
|
// For the last block, mask out the garbage bits beyond chunk._count
|
||||||
|
var validMask = ulong.MaxValue;
|
||||||
|
var remaining = chunk._count - (block * 64);
|
||||||
|
if (remaining < 64)
|
||||||
|
{
|
||||||
|
validMask = (1UL << remaining) - 1UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersect requireEnabled (Bits MUST be 1)
|
||||||
|
var it = _mask.requireEnabled.GetIterator();
|
||||||
|
while (it.Next(out var id) && validMask != 0)
|
||||||
|
{
|
||||||
|
var layoutResult = archetype.GetLayout(id);
|
||||||
|
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
|
||||||
|
{
|
||||||
|
var pMask = (ulong*)(chunk.GetUnsafePtr() + layoutResult.Value.enableBitsOffset);
|
||||||
|
validMask &= pMask[block];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersect requireDisabled / rejectIfEnabled (Bits MUST be 0)
|
||||||
|
it = _mask.requireDisabled.GetIterator();
|
||||||
|
while (it.Next(out var id) && validMask != 0)
|
||||||
|
{
|
||||||
|
var layoutResult = archetype.GetLayout(id);
|
||||||
|
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
|
||||||
|
{
|
||||||
|
var pMask = (ulong*)(chunk.GetUnsafePtr() + layoutResult.Value.enableBitsOffset);
|
||||||
|
validMask &= ~pMask[block]; // Invert memory, because it must be 0!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rejectIfEnabled behaves identically to requireDisabled
|
||||||
|
it = _mask.rejectIfEnabled.GetIterator();
|
||||||
|
while (it.Next(out var id) && validMask != 0)
|
||||||
|
{
|
||||||
|
var layoutResult = archetype.GetLayout(id);
|
||||||
|
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
|
||||||
|
{
|
||||||
|
var pMask = (ulong*)(chunk.GetUnsafePtr() + layoutResult.Value.enableBitsOffset);
|
||||||
|
validMask &= ~pMask[block];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If validMask still has ANY bits set to 1, we found at least one matching entity.
|
||||||
|
if (validMask != 0)
|
||||||
|
{
|
||||||
|
chunkHasMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkHasMatch)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
|
|||||||
var registerTypeName = "g_assethandler_registeration";
|
var registerTypeName = "g_assethandler_registeration";
|
||||||
var code = $@"// <auto-generated />
|
var code = $@"// <auto-generated />
|
||||||
|
|
||||||
#if GHOST_SAFETY_CHECKS
|
#if GHOST_EDITOR
|
||||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||||
internal static partial class {registerTypeName}
|
internal static partial class {registerTypeName}
|
||||||
{{
|
{{
|
||||||
@@ -159,7 +159,7 @@ internal class IAssetSettingsRegistrationGenerator : IIncrementalGenerator
|
|||||||
var registerTypeName = "g_iassetsettings_registeration";
|
var registerTypeName = "g_iassetsettings_registeration";
|
||||||
var code = $@"// <auto-generated />
|
var code = $@"// <auto-generated />
|
||||||
|
|
||||||
#if GHOST_SAFETY_CHECKS
|
#if GHOST_EDITOR
|
||||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||||
internal static partial class {registerTypeName}
|
internal static partial class {registerTypeName}
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()}
|
|||||||
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
|
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
|
||||||
{info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name}
|
{info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name}
|
||||||
{{
|
{{
|
||||||
#if GHOST_SAFETY_CHECKS
|
#if GHOST_EDITOR
|
||||||
public const string HLSL_SOURCE = @""
|
public const string HLSL_SOURCE = @""
|
||||||
struct {info.Name}
|
struct {info.Name}
|
||||||
{{
|
{{
|
||||||
@@ -222,7 +222,7 @@ struct {info.Name}
|
|||||||
var registerTypeName = "g_shaderproperty_registeration";
|
var registerTypeName = "g_shaderproperty_registeration";
|
||||||
var registerCode = $@"// <auto-generated/>
|
var registerCode = $@"// <auto-generated/>
|
||||||
|
|
||||||
#if GHOST_SAFETY_CHECKS
|
#if GHOST_EDITOR
|
||||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||||
internal static partial class {registerTypeName}
|
internal static partial class {registerTypeName}
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public unsafe struct ShaderByteCode
|
|||||||
public ulong size;
|
public ulong size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe delegate void ShaderVariantCompiledHandler(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> byteCodes);
|
public delegate void ShaderVariantCompiledHandler(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> byteCodes);
|
||||||
|
|
||||||
public interface IShaderCompilationBridge : IDisposable
|
public interface IShaderCompilationBridge : IDisposable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,11 +43,11 @@
|
|||||||
<ProjectCapability Include="Msix" />
|
<ProjectCapability Include="Msix" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.4.0" />
|
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.5.1" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="4.2.1" />
|
<PackageReference Include="MSTest.TestAdapter" Version="4.2.3" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="4.2.1" />
|
<PackageReference Include="MSTest.TestFramework" Version="4.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
|
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MSTest" Version="4.2.2" />
|
<PackageReference Include="MSTest" Version="4.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ internal class MockingContentProvider : IContentProvider
|
|||||||
{
|
{
|
||||||
var header = new TextureContentHeader
|
var header = new TextureContentHeader
|
||||||
{
|
{
|
||||||
|
magic = TextureContentHeader.MAGIC,
|
||||||
|
version = TextureContentHeader.VERSION,
|
||||||
width = width,
|
width = width,
|
||||||
height = height,
|
height = height,
|
||||||
bpc = 8,
|
bpc = 8,
|
||||||
@@ -144,14 +146,14 @@ internal class MockingContentProvider : IContentProvider
|
|||||||
};
|
};
|
||||||
|
|
||||||
stream.Write(header);
|
stream.Write(header);
|
||||||
header.vertexOffset = (ulong)stream.Position; stream.Write(vertices);
|
header.vertexOffset = stream.Position; stream.Write(vertices);
|
||||||
header.indexOffset = (ulong)stream.Position; stream.Write(indices);
|
header.indexOffset = stream.Position; stream.Write(indices);
|
||||||
header.materialPartOffset = (ulong)stream.Position; stream.Write(materialParts);
|
header.materialPartOffset = stream.Position; stream.Write(materialParts);
|
||||||
header.meshletOffset = (ulong)stream.Position; stream.Write(meshlets);
|
header.meshletOffset = stream.Position; stream.Write(meshlets);
|
||||||
header.meshletGroupOffset = (ulong)stream.Position; stream.Write(groups);
|
header.meshletGroupOffset = stream.Position; stream.Write(groups);
|
||||||
header.meshletHierarchyNodeOffset = (ulong)stream.Position; stream.Write(hierarchy);
|
header.meshletHierarchyNodeOffset = stream.Position; stream.Write(hierarchy);
|
||||||
header.meshletVertexOffset = (ulong)stream.Position; stream.Write(meshletVertices);
|
header.meshletVertexOffset = stream.Position; stream.Write(meshletVertices);
|
||||||
header.meshletTriangleOffset = (ulong)stream.Position; stream.Write(meshletTriangles);
|
header.meshletTriangleOffset = stream.Position; stream.Write(meshletTriangles);
|
||||||
|
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
stream.Write(header);
|
stream.Write(header);
|
||||||
|
|||||||
143
src/Test/Ghost.UnitTest/SceneGraphSyncTests.cs
Normal file
143
src/Test/Ghost.UnitTest/SceneGraphSyncTests.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
|
using Ghost.Engine;
|
||||||
|
using Ghost.Engine.Components;
|
||||||
|
using Ghost.Engine.Core;
|
||||||
|
using Ghost.Entities;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
[DoNotParallelize]
|
||||||
|
public class SceneGraphSyncTests
|
||||||
|
{
|
||||||
|
private EditorWorldService _worldService = null!;
|
||||||
|
private SceneGraphSyncService _syncService = null!;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_worldService = new EditorWorldService();
|
||||||
|
_syncService = new SceneGraphSyncService(_worldService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_syncService.Dispose();
|
||||||
|
_worldService.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void SceneGraph_InitialBuild_PopulatesTreeCorrectly()
|
||||||
|
{
|
||||||
|
var world = _worldService.EditorWorld;
|
||||||
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
|
// Create parent and child
|
||||||
|
var parent = world.EntityManager.CreateEntity();
|
||||||
|
world.EntityManager.AddSharedComponent(parent, new SceneID { value = scene.ID });
|
||||||
|
world.EntityManager.AddComponent(parent, Hierarchy.Root);
|
||||||
|
|
||||||
|
var child = world.EntityManager.CreateEntity();
|
||||||
|
world.EntityManager.AddSharedComponent(child, new SceneID { value = scene.ID });
|
||||||
|
world.EntityManager.AddComponent(child, Hierarchy.Root);
|
||||||
|
|
||||||
|
HierarchyUtility.SetParent(world, child, parent);
|
||||||
|
|
||||||
|
var names = new Dictionary<Entity, string>
|
||||||
|
{
|
||||||
|
{ parent, "ParentEntity" },
|
||||||
|
{ child, "ChildEntity" }
|
||||||
|
};
|
||||||
|
|
||||||
|
_worldService.RebuildSceneGraph(names);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, _worldService.RootNodes.Count);
|
||||||
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
|
Assert.AreEqual(scene.ID, sceneNode.Scene.ID);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, sceneNode.Children.Count);
|
||||||
|
var parentNode = (EntityNode)sceneNode.Children[0];
|
||||||
|
Assert.AreEqual("ParentEntity", parentNode.Name);
|
||||||
|
Assert.AreEqual(parent, parentNode.Entity);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, parentNode.Children.Count);
|
||||||
|
var childNode = (EntityNode)parentNode.Children[0];
|
||||||
|
Assert.AreEqual("ChildEntity", childNode.Name);
|
||||||
|
Assert.AreEqual(child, childNode.Entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void SceneGraph_CreateEntity_AppendsToRootAutomatically()
|
||||||
|
{
|
||||||
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
|
var entity = _worldService.CreateEntity("NewEntity", scene.ID);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, _worldService.RootNodes.Count);
|
||||||
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
|
Assert.AreEqual(1, sceneNode.Children.Count);
|
||||||
|
var entityNode = (EntityNode)sceneNode.Children[0];
|
||||||
|
Assert.AreEqual("NewEntity", entityNode.Name);
|
||||||
|
Assert.AreEqual(entity, entityNode.Entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void SceneGraph_DestroyEntity_RemovesFromTreeAutomatically()
|
||||||
|
{
|
||||||
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
|
var entity = _worldService.CreateEntity("NewEntity", scene.ID);
|
||||||
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
|
Assert.AreEqual(1, sceneNode.Children.Count);
|
||||||
|
|
||||||
|
_worldService.DestroyEntity(entity);
|
||||||
|
|
||||||
|
Assert.AreEqual(0, sceneNode.Children.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void SceneGraph_SetParent_MovesNodeInTree()
|
||||||
|
{
|
||||||
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
|
var parent = _worldService.CreateEntity("Parent", scene.ID);
|
||||||
|
var child = _worldService.CreateEntity("Child", scene.ID);
|
||||||
|
|
||||||
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
|
Assert.AreEqual(2, sceneNode.Children.Count);
|
||||||
|
|
||||||
|
var err = _worldService.SetParent(child, parent);
|
||||||
|
Assert.AreEqual(Error.None, err);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, sceneNode.Children.Count);
|
||||||
|
var parentNode = (EntityNode)sceneNode.Children[0];
|
||||||
|
Assert.AreEqual("Parent", parentNode.Name);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, parentNode.Children.Count);
|
||||||
|
var childNode = (EntityNode)parentNode.Children[0];
|
||||||
|
Assert.AreEqual("Child", childNode.Name);
|
||||||
|
Assert.AreEqual(child, childNode.Entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void SceneGraph_RenameEntity_UpdatesNodeNameInstantly()
|
||||||
|
{
|
||||||
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
|
var entity = _worldService.CreateEntity("OriginalName", scene.ID);
|
||||||
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
|
var entityNode = (EntityNode)sceneNode.Children[0];
|
||||||
|
|
||||||
|
Assert.AreEqual("OriginalName", entityNode.Name);
|
||||||
|
|
||||||
|
_worldService.RenameEntity(entity, "NewName");
|
||||||
|
|
||||||
|
Assert.AreEqual("NewName", entityNode.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,8 @@ public class SceneSerializationTests
|
|||||||
EditorApplication.Initialize(new EmptyServiceProvider(), _projectRoot, "SceneTest");
|
EditorApplication.Initialize(new EmptyServiceProvider(), _projectRoot, "SceneTest");
|
||||||
|
|
||||||
_worldService = new EditorWorldService();
|
_worldService = new EditorWorldService();
|
||||||
_serializationService = new SceneSerializationService(_worldService, null!);
|
var syncService = new SceneGraphSyncService(_worldService);
|
||||||
|
_serializationService = new SceneSerializationService(_worldService, null!, syncService);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TestCleanup]
|
||||||
|
|||||||
Reference in New Issue
Block a user