Remove old SceneGraph
This commit is contained in:
@@ -2,10 +2,10 @@ namespace Ghost.Editor.Core.Resources;
|
|||||||
|
|
||||||
internal static class FileExtensions
|
internal static class FileExtensions
|
||||||
{
|
{
|
||||||
public const string PROJECT_FILE_EXTENSION = ".ghostproj";
|
public const string PROJECT_FILE_EXTENSION = ".gproj";
|
||||||
public const string TEMPLATE_FILE_EXTENSION = ".ghosttemplate";
|
public const string TEMPLATE_FILE_EXTENSION = ".gtmpl";
|
||||||
public const string SCENE_FILE_EXTENSION = ".ghostscene";
|
public const string SCENE_FILE_EXTENSION = ".gscene";
|
||||||
public const string ASSET_FILE_EXTENSION = ".ghostasset";
|
public const string ASSET_FILE_EXTENSION = ".gasset";
|
||||||
public const string SHADER_FILE_EXTENSION = ".ghostshader";
|
public const string SHADER_FILE_EXTENSION = ".gshdr";
|
||||||
public const string MATERIAL_FILE_EXTENSION = ".ghostmaterial";
|
public const string MATERIAL_FILE_EXTENSION = ".gmat";
|
||||||
}
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using Ghost.Editor.Core.Progress;
|
|
||||||
using Ghost.Editor.Core.Resources;
|
|
||||||
using Ghost.Editor.Core.Serializer;
|
|
||||||
using Ghost.Editor.Core.Utilities;
|
|
||||||
using Ghost.Engine.Core;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Editor-specific scene manager that uses JSON serialization for human-readable scene files.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This manager provides JSON-based serialization suitable for editor workflows.
|
|
||||||
/// Runtime applications should use SceneManager with binary serialization.
|
|
||||||
/// </remarks>
|
|
||||||
public static class EditorSceneManager
|
|
||||||
{
|
|
||||||
// TODO: Use guid keys instead of string paths for better performance and uniqueness
|
|
||||||
private static readonly Dictionary<string, SceneNode> s_loadedWorlds = new();
|
|
||||||
public static IEnumerable<SceneNode> LoadedWorlds => s_loadedWorlds.Values;
|
|
||||||
|
|
||||||
public static event Action<SceneNode>? OnWorldLoaded;
|
|
||||||
public static event Action<SceneNode>? OnWorldUnloaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads a scene from a JSON file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="worldPath">The path to the JSON scene file.</param>
|
|
||||||
public static async Task LoadSceneAsync(string worldPath)
|
|
||||||
{
|
|
||||||
if (s_loadedWorlds.ContainsKey(worldPath)
|
|
||||||
|| !File.Exists(worldPath)
|
|
||||||
|| Path.GetExtension(worldPath) != FileExtensions.SCENE_FILE_EXTENSION)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var progressService = EditorApplication.GetService<IProgressService>();
|
|
||||||
progressService.ShowIndeterminateProgress("Loading world...");
|
|
||||||
|
|
||||||
// Unload existing scenes
|
|
||||||
foreach (var world in s_loadedWorlds)
|
|
||||||
{
|
|
||||||
world.Value.Unload();
|
|
||||||
OnWorldUnloaded?.Invoke(world.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
s_loadedWorlds.Clear();
|
|
||||||
|
|
||||||
// TODO: Get or create World instance for editor
|
|
||||||
// For now, keep compatibility with old SceneNode deserialization
|
|
||||||
await using var readStream = new FileStream(worldPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
var deserializedScene = await JsonSerializer.DeserializeAsync<SceneNode>(readStream, Engine.Resources.EngineResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed.");
|
|
||||||
|
|
||||||
s_loadedWorlds[worldPath] = deserializedScene;
|
|
||||||
await deserializedScene.LoadAsync();
|
|
||||||
|
|
||||||
progressService.HideProgress();
|
|
||||||
OnWorldLoaded?.Invoke(deserializedScene);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves a scene to a JSON file using the new serializer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scene">The scene to save.</param>
|
|
||||||
/// <param name="filePath">The path to save the JSON scene file.</param>
|
|
||||||
public static async Task SaveSceneAsync(Scene scene, string filePath)
|
|
||||||
{
|
|
||||||
await SceneSerializer.SaveSceneAsync(scene.World, scene.ID, filePath, scene.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
using Ghost.Editor.Core.Controls.Internal;
|
|
||||||
using Ghost.Editor.Core.Inspector;
|
|
||||||
using Ghost.Editor.Core.Resources;
|
|
||||||
using Ghost.Engine.Editor;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using Microsoft.UI.Text;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.UI.Xaml.Data;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
|
||||||
|
|
||||||
public partial class EntityNode : SceneGraphNode
|
|
||||||
{
|
|
||||||
public SceneNode Owner
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Entity Entity
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override SceneGraphNodeType NodeType => SceneGraphNodeType.Entity;
|
|
||||||
|
|
||||||
public EntityNode(SceneNode owner, Entity entity, string name)
|
|
||||||
{
|
|
||||||
Owner = owner;
|
|
||||||
Entity = entity;
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class EntityNode : IInspectable
|
|
||||||
{
|
|
||||||
public IconSource? Icon => EditorIconSource.entity_24;
|
|
||||||
|
|
||||||
public UIElement? HeaderContent
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var root = new StackPanel()
|
|
||||||
{
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center
|
|
||||||
};
|
|
||||||
|
|
||||||
var nameText = new TextBox
|
|
||||||
{
|
|
||||||
Text = Name,
|
|
||||||
FontWeight = FontWeights.Bold,
|
|
||||||
};
|
|
||||||
var idText = new TextBlock
|
|
||||||
{
|
|
||||||
Text = $"ID: {Entity.ID} Generation: {Entity.Generation}",
|
|
||||||
Margin = new Thickness(5, 7, 0, 0),
|
|
||||||
Opacity = 0.75,
|
|
||||||
Style = Application.Current.Resources["CaptionTextBlockStyle"] as Style
|
|
||||||
};
|
|
||||||
|
|
||||||
nameText.SetBinding(TextBox.TextProperty, new Binding
|
|
||||||
{
|
|
||||||
Source = this,
|
|
||||||
Path = new PropertyPath(nameof(Name)),
|
|
||||||
Mode = BindingMode.TwoWay,
|
|
||||||
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
|
|
||||||
});
|
|
||||||
|
|
||||||
root.Children.Add(nameText);
|
|
||||||
root.Children.Add(idText);
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe UIElement? InspectorContent
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var r = Owner.Scene.World.EntityManager.GetEntityLocation(Entity);
|
|
||||||
if (!r)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var root = new StackPanel()
|
|
||||||
{
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
||||||
VerticalAlignment = VerticalAlignment.Top
|
|
||||||
};
|
|
||||||
|
|
||||||
var location = r.Value;
|
|
||||||
ref var archetype = ref Owner.Scene.World.ComponentManager.GetArchetypeReference(location.archetypeID);
|
|
||||||
|
|
||||||
var it = archetype._signature.GetIterator();
|
|
||||||
while (it.Next(out var typeID))
|
|
||||||
{
|
|
||||||
var pComponent = archetype.GetComponentData(location.chunkIndex, location.rowIndex, typeID);
|
|
||||||
if (pComponent == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ComponentRegistry.s_runtimeIDToType.TryGetValue(typeID, out var t))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.GetCustomAttribute<HideEditorAttribute>() != null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var componentView = new ComponentView(t.Name, Owner.Scene.World, Entity, t);
|
|
||||||
root.Children.Add(componentView);
|
|
||||||
}
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
68
Ghost.Editor.Core/SceneGraph/SceneGraph Plan.md
Normal file
68
Ghost.Editor.Core/SceneGraph/SceneGraph Plan.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Architecture Plan: Scene Graph and Scene Representation
|
||||||
|
|
||||||
|
The Scene Graph is a hierarchical structure that represents all the objects and entities within a 3D scene in the Ghost Editor.
|
||||||
|
|
||||||
|
## Scene Graph (Editor representation of runtime data)
|
||||||
|
|
||||||
|
There should be two main types of nodes in the Scene Graph:
|
||||||
|
1. **Entity Node**: Represents an individual entity within a scene.
|
||||||
|
2. **Scene Node**: Represents a Scene object, which can contain multiple entities.
|
||||||
|
|
||||||
|
### Editor World
|
||||||
|
|
||||||
|
Editor contains a different world compares to the runtime world. When user click the Play button, we will create a runtime world and load the scene data from the editor world to the runtime world.
|
||||||
|
This allows us to
|
||||||
|
1. Unload the runtime only systems like physics, rendering, etc when user stop playing.
|
||||||
|
2. Load editor only systems like gizmos, debug, etc when user stop playing.
|
||||||
|
3. Allow editor only entities like editor camera, editor lights, etc to exist in the editor world without affecting the runtime world.
|
||||||
|
|
||||||
|
### Editor Hierarchy
|
||||||
|
|
||||||
|
The Scene Graph should be represented as a tree structure in the editor (TreeView in WinUI 3), where:
|
||||||
|
- The top level nodes represents the loaded Scenes in the editor world.
|
||||||
|
- Levels below the Scene nodes represents the Entity nodes that belong to that scene.
|
||||||
|
- Each Entity node can have child Entity nodes representing parent-child relationships between entities.
|
||||||
|
|
||||||
|
An example hierarchy could look like this:
|
||||||
|
```
|
||||||
|
- Scene 1
|
||||||
|
- Entity A
|
||||||
|
- Entity B
|
||||||
|
- Entity C
|
||||||
|
- Scene 2
|
||||||
|
- Entity D
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scene (The runtime representation)
|
||||||
|
|
||||||
|
A Scene is a collection of entities with SceneID component from a world that are grouped together. There can be multiple scenes in a world.
|
||||||
|
|
||||||
|
### Save a Scene
|
||||||
|
When save a scene, all entities with the SceneID component matching the scene's ID should be included in the saved data.
|
||||||
|
When an Entity references another Entity in the same scene, we should store the file local id instead of the global entity id.
|
||||||
|
For example, if Entity A (id: 10, 5th in scene) references Entity B (id: 20, 50th in scene) in the same scene, in the saved data for Entity A,
|
||||||
|
we should store 50 (the file local id) as the reference to Entity B instead of 20 (the global entity id).
|
||||||
|
|
||||||
|
> We does not allow cross-scene references for now because ideally it's not a good practice to have cross-scene references.
|
||||||
|
We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
|
||||||
|
|
||||||
|
### Load a Scene
|
||||||
|
When loading a scene, we need to reconstruct the entities and their relationships based on the saved data.
|
||||||
|
|
||||||
|
1. We allocate the entities in the world and assign them new global entity IDs.
|
||||||
|
2. We remap the file local IDs to the new global entity IDs and change the references accordingly.
|
||||||
|
For example if Entity A (file local id: 5) references Entity B (file local id: 50) in the saved data,
|
||||||
|
we need to find the new global entity IDs for both entities after loading and update the reference in Entity A to point to the new global entity ID of Entity B.
|
||||||
|
|
||||||
|
### Data format
|
||||||
|
The scene data should be stored in a structured format (e.g., JSON or binary) that includes:
|
||||||
|
- Scene metadata (e.g., name, ID)
|
||||||
|
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
|
||||||
|
- References between entities using file local IDs
|
||||||
|
|
||||||
|
JSON should only be used in the editor and JSON serialization/deserialization logic should also only exist in the editor codebase (Ghost.Editor.Core). Reflection is allowed here.
|
||||||
|
Binary format should be used in the runtime for better performance. The runtime codebase (Ghost.Engine) must be aot compatible.
|
||||||
|
|
||||||
|
Currently we strict the IComponent to must be unmanaged and blittable types.
|
||||||
|
However, we also support ManagedEntity and ManagedEntityRef with ScriptComponent to allow OOP like logic for common gameplay logic that DOD pattern is not suitable for.
|
||||||
|
Serializing/deserializing with those components will be tricky. We can use MemoryPack for binary serialization/deserialization because it supports both unmanaged and managed types.
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
using Ghost.Engine.Components;
|
|
||||||
using Ghost.Entities;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
|
||||||
|
|
||||||
public class SceneGraphHelpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="EntityNode"/> entity with default components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world context where the entity will be created.</param>
|
|
||||||
/// <param name="entity">The entity to be wrapped in the <see cref="EntityNode"/>.</param>
|
|
||||||
public static EntityNode CreateEntityNode(SceneNode owner, Entity entity, string name)
|
|
||||||
{
|
|
||||||
owner.Scene.World.EntityManager.AddComponent(entity, new LocalToWorld { matrix = Misaki.HighPerformance.Mathematics.float4x4.identity });
|
|
||||||
owner.Scene.World.EntityManager.AddComponent(entity, Hierarchy.Root);
|
|
||||||
return new EntityNode(owner, entity, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="Entity"/> and <see cref="EntityNode"/> entity with default components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="owner">The world context where the entity will be created.</param>
|
|
||||||
public static EntityNode CreateEntityNode(SceneNode owner, string name)
|
|
||||||
{
|
|
||||||
var entity = owner.Scene.World.EntityManager.CreateEntity();
|
|
||||||
return CreateEntityNode(owner, entity, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attaches childEntity to parentEntity in the scene graph.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world context where the entities exist.</param>
|
|
||||||
/// <param name="parentNode">The parent entity to which the child will be attached.</param>
|
|
||||||
/// <param name="childNode">The child entity to be attached.</param>
|
|
||||||
public static void AttachChild(SceneNode scene, EntityNode parentNode, EntityNode childNode)
|
|
||||||
{
|
|
||||||
// 1) If the child already has a parent, detach it first
|
|
||||||
var childHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(childNode.Entity);
|
|
||||||
if (childHierarchy.parent != Entity.Invalid)
|
|
||||||
{
|
|
||||||
DetachFromParent(scene, childNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Link child to new parent
|
|
||||||
childHierarchy.parent = parentNode.Entity;
|
|
||||||
|
|
||||||
// 3) Insert child at the head of parent's child list
|
|
||||||
var parentHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(parentNode.Entity);
|
|
||||||
|
|
||||||
childHierarchy.nextSibling = parentHierarchy.firstChild;
|
|
||||||
parentHierarchy.firstChild = childNode.Entity;
|
|
||||||
|
|
||||||
// 4) Write back
|
|
||||||
scene.Scene.World.EntityManager.SetComponent(parentNode.Entity, parentHierarchy);
|
|
||||||
scene.Scene.World.EntityManager.SetComponent(childNode.Entity, childHierarchy);
|
|
||||||
|
|
||||||
// 5) Update children list in parent node
|
|
||||||
parentNode.AddChild(childNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Detaches the specified entity from its parent in the scene graph.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world context where the entities exist.</param>
|
|
||||||
/// <param name="node">The entity to detach from its parent.</param>
|
|
||||||
public static void DetachFromParent(SceneNode scene, EntityNode node)
|
|
||||||
{
|
|
||||||
var hierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(node.Entity);
|
|
||||||
var parent = hierarchy.parent;
|
|
||||||
if (parent == Entity.Invalid)
|
|
||||||
{
|
|
||||||
return; // already root
|
|
||||||
}
|
|
||||||
|
|
||||||
var parentHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(parent);
|
|
||||||
|
|
||||||
// If entity is the first child, simply move head
|
|
||||||
if (parentHierarchy.firstChild == node.Entity)
|
|
||||||
{
|
|
||||||
parentHierarchy.firstChild = hierarchy.nextSibling;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Otherwise, find the previous sibling in the linked list
|
|
||||||
var prevSibling = parentHierarchy.firstChild;
|
|
||||||
while (prevSibling != Entity.Invalid)
|
|
||||||
{
|
|
||||||
var prevHierarchy = scene.Scene.World.EntityManager.GetComponent<Hierarchy>(prevSibling);
|
|
||||||
if (prevHierarchy.nextSibling == node.Entity)
|
|
||||||
{
|
|
||||||
prevHierarchy.nextSibling = hierarchy.nextSibling;
|
|
||||||
scene.Scene.World.EntityManager.SetComponent(prevSibling, prevHierarchy);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevSibling = prevHierarchy.nextSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear child's references
|
|
||||||
hierarchy.parent = Entity.Invalid;
|
|
||||||
hierarchy.nextSibling = Entity.Invalid;
|
|
||||||
|
|
||||||
// Write back
|
|
||||||
scene.Scene.World.EntityManager.SetComponent(parent, parentHierarchy);
|
|
||||||
scene.Scene.World.EntityManager.SetComponent(node.Entity, hierarchy);
|
|
||||||
|
|
||||||
// Remove from parent's children list
|
|
||||||
scene.EntityNodeLookup[parent].RemoveChild(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
|
||||||
|
|
||||||
public enum SceneGraphNodeType
|
|
||||||
{
|
|
||||||
Scene,
|
|
||||||
Entity,
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract partial class SceneGraphNode : ObservableObject
|
|
||||||
{
|
|
||||||
public ObservableCollection<SceneGraphNode>? Children
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string Name
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract SceneGraphNodeType NodeType
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ChildCount => Children?.Count ?? 0;
|
|
||||||
|
|
||||||
public virtual void AddChild(SceneGraphNode child)
|
|
||||||
{
|
|
||||||
Children ??= new();
|
|
||||||
Children.Add(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual bool RemoveChild(SceneGraphNode child)
|
|
||||||
{
|
|
||||||
return Children?.Remove(child) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SceneGraphNode GetChild(int index)
|
|
||||||
{
|
|
||||||
if (Children == null || index < 0 || index >= Children.Count)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Children[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
using Ghost.Editor.Core.AssetHandle;
|
|
||||||
using Ghost.Editor.Core.Inspector;
|
|
||||||
using Ghost.Editor.Core.Resources;
|
|
||||||
using Ghost.Engine.Components;
|
|
||||||
using Ghost.Engine.Core;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
|
||||||
|
|
||||||
public partial class SceneNode : SceneGraphNode, IEquatable<SceneNode>
|
|
||||||
{
|
|
||||||
private Scene _scene;
|
|
||||||
private Dictionary<Entity, EntityNode> _entityNodeLookup = new();
|
|
||||||
|
|
||||||
public Scene Scene => _scene;
|
|
||||||
public Dictionary<Entity, EntityNode> EntityNodeLookup => _entityNodeLookup;
|
|
||||||
|
|
||||||
public override SceneGraphNodeType NodeType => SceneGraphNodeType.Scene;
|
|
||||||
|
|
||||||
public SceneNode(Scene scene, string name)
|
|
||||||
{
|
|
||||||
_scene = scene;
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLookup(Entity key, EntityNode value)
|
|
||||||
{
|
|
||||||
_entityNodeLookup[key] = value;
|
|
||||||
if (value.Children == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in value.Children)
|
|
||||||
{
|
|
||||||
if (child is EntityNode entityChild)
|
|
||||||
{
|
|
||||||
UpdateLookup(entityChild.Entity, entityChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void AddChild(SceneGraphNode child)
|
|
||||||
{
|
|
||||||
if (child is not EntityNode entityNode)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Child must be of type EntityNode.", nameof(child));
|
|
||||||
}
|
|
||||||
|
|
||||||
base.AddChild(entityNode);
|
|
||||||
UpdateLookup(entityNode.Entity, entityNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool RemoveChild(SceneGraphNode child)
|
|
||||||
{
|
|
||||||
if (child is not EntityNode entityNode)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Child must be of type EntityNode.", nameof(child));
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = base.RemoveChild(child);
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
_entityNodeLookup.Remove(entityNode.Entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EntityNode BuildNodeRecursive(Entity entity)
|
|
||||||
{
|
|
||||||
if (!_entityNodeLookup.TryGetValue(entity, out var node))
|
|
||||||
{
|
|
||||||
node = new EntityNode(this, entity, "New Entity");
|
|
||||||
_entityNodeLookup[entity] = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hc = _scene.World.EntityManager.GetComponent<Hierarchy>(entity);
|
|
||||||
var child = hc.firstChild;
|
|
||||||
|
|
||||||
while (child != Entity.Invalid)
|
|
||||||
{
|
|
||||||
node.AddChild(BuildNodeRecursive(child));
|
|
||||||
var childHC = _scene.World.EntityManager.GetComponent<Hierarchy>(child);
|
|
||||||
child = childHC.nextSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildGraph()
|
|
||||||
{
|
|
||||||
var queryID = new QueryBuilder()
|
|
||||||
.WithAll<Hierarchy>()
|
|
||||||
.Build(_scene.World);
|
|
||||||
|
|
||||||
_scene.World.ComponentManager.GetEntityQueryReference(queryID).ForEach<Hierarchy>((entity, ref hierarchy) =>
|
|
||||||
{
|
|
||||||
if (hierarchy.parent == Entity.Invalid)
|
|
||||||
{
|
|
||||||
var node = BuildNodeRecursive(entity);
|
|
||||||
AddChild(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task LoadAsync()
|
|
||||||
{
|
|
||||||
return Task.Run(BuildGraph);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Unload()
|
|
||||||
{
|
|
||||||
_scene = null!;
|
|
||||||
|
|
||||||
Children?.Clear();
|
|
||||||
_entityNodeLookup.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"WorldNode: {Name} (World ID: {_scene.ID})";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return HashCode.Combine(_scene, Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
return obj is SceneNode other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(SceneNode? other)
|
|
||||||
{
|
|
||||||
if (other is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ReferenceEquals(this, other))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _scene.Equals(other._scene) && Name == other.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(SceneNode? left, SceneNode? right)
|
|
||||||
{
|
|
||||||
if (left is null)
|
|
||||||
{
|
|
||||||
return right is null;
|
|
||||||
}
|
|
||||||
return left.Equals(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(SceneNode? left, SceneNode? right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class SceneNode : IInspectable
|
|
||||||
{
|
|
||||||
public IconSource? Icon => EditorIconSource.scene_24;
|
|
||||||
|
|
||||||
[AssetOpenHandler(FileExtensions.SCENE_FILE_EXTENSION)]
|
|
||||||
public static async void Open(string path)
|
|
||||||
{
|
|
||||||
await EditorSceneManager.LoadSceneAsync(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UIElement? HeaderContent => null;
|
|
||||||
|
|
||||||
public UIElement? InspectorContent => null;
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
using Ghost.Entities;
|
|
||||||
using Ghost.Engine.IO;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Serializer.Converters;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// JSON converter for Entity that handles automatic ID remapping during deserialization.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// During serialization, writes the file-local entity ID.
|
|
||||||
/// During deserialization, reads the file-local ID and translates it to the runtime Entity
|
|
||||||
/// using the active SerializationContext.
|
|
||||||
/// </remarks>
|
|
||||||
public class EntityJsonConverter : JsonConverter<Entity>
|
|
||||||
{
|
|
||||||
public override Entity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
{
|
|
||||||
return Entity.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.StartObject)
|
|
||||||
{
|
|
||||||
throw new JsonException("Expected StartObject token for Entity.");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fileId = -1;
|
|
||||||
int generation = -1;
|
|
||||||
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.EndObject)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
|
||||||
{
|
|
||||||
var propertyName = reader.GetString();
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
switch (propertyName)
|
|
||||||
{
|
|
||||||
case "ID":
|
|
||||||
case "id":
|
|
||||||
fileId = reader.GetInt32();
|
|
||||||
break;
|
|
||||||
case "Generation":
|
|
||||||
case "generation":
|
|
||||||
generation = reader.GetInt32();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileId == Entity.INVALID_ID)
|
|
||||||
{
|
|
||||||
return Entity.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a serialization context, remap the file ID to runtime entity
|
|
||||||
var context = SerializationContext.Current;
|
|
||||||
if (context != null)
|
|
||||||
{
|
|
||||||
if (context.TryGetEntity(fileId, out var runtimeEntity))
|
|
||||||
{
|
|
||||||
return runtimeEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If entity not found in map, return invalid
|
|
||||||
return Entity.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No context means we're not in a deserialization scope - should not happen
|
|
||||||
throw new InvalidOperationException("Entity deserialization requires an active SerializationContext.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, Entity value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (!value.IsValid)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteStartObject();
|
|
||||||
|
|
||||||
// If we have a serialization context, write the file-local ID
|
|
||||||
var context = SerializationContext.Current;
|
|
||||||
if (context != null)
|
|
||||||
{
|
|
||||||
if (context.TryGetFileId(value, out var fileId))
|
|
||||||
{
|
|
||||||
writer.WriteNumber("ID", fileId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Entity not in context - register it now
|
|
||||||
var newFileId = context.RegisterEntityForSerialization(value);
|
|
||||||
writer.WriteNumber("ID", newFileId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No context - write the runtime ID (for debugging or non-scene serialization)
|
|
||||||
writer.WriteNumber("ID", value.ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteNumber("Generation", value.Generation);
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
using Ghost.Core;
|
|
||||||
using Ghost.Editor.Core.Serializer.Converters;
|
|
||||||
using Ghost.Engine.Components;
|
|
||||||
using Ghost.Engine.Core;
|
|
||||||
using Ghost.Engine.IO;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Serializer;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles JSON serialization and deserialization of scenes.
|
|
||||||
/// </summary>
|
|
||||||
public static class SceneSerializer
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions s_serializerOptions;
|
|
||||||
|
|
||||||
static SceneSerializer()
|
|
||||||
{
|
|
||||||
s_serializerOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
IncludeFields = true,
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new EntityJsonConverter(),
|
|
||||||
new JsonStringEnumConverter()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the serialized data for a single entity.
|
|
||||||
/// </summary>
|
|
||||||
private class SerializedEntity
|
|
||||||
{
|
|
||||||
public int FileID { get; set; }
|
|
||||||
public List<SerializedComponent> Components { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a serialized component with its type and data.
|
|
||||||
/// </summary>
|
|
||||||
private class SerializedComponent
|
|
||||||
{
|
|
||||||
public string TypeName { get; set; } = string.Empty;
|
|
||||||
public JsonElement Data { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the complete scene file structure.
|
|
||||||
/// </summary>
|
|
||||||
private class SceneFile
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "Untitled Scene";
|
|
||||||
public int Version { get; set; } = 1;
|
|
||||||
public List<SerializedEntity> Entities { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves a scene to a JSON file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world containing the entities.</param>
|
|
||||||
/// <param name="sceneID">The scene ID to save.</param>
|
|
||||||
/// <param name="filePath">The path to save the scene file.</param>
|
|
||||||
/// <param name="sceneName">Optional scene name.</param>
|
|
||||||
public static async Task SaveSceneAsync(World world, short sceneID, string filePath, string? sceneName = null)
|
|
||||||
{
|
|
||||||
using var context = SerializationContext.Create();
|
|
||||||
|
|
||||||
var sceneFile = new SceneFile
|
|
||||||
{
|
|
||||||
Name = sceneName ?? Path.GetFileNameWithoutExtension(filePath),
|
|
||||||
Entities = new List<SerializedEntity>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Query all entities with the specified SceneID
|
|
||||||
var queryID = new QueryBuilder()
|
|
||||||
.WithAll<SceneID>()
|
|
||||||
.Build(world);
|
|
||||||
|
|
||||||
var entities = new List<Entity>();
|
|
||||||
|
|
||||||
world.ComponentManager.GetEntityQueryReference(queryID).ForEach<SceneID>((Entity entity, ref SceneID sceneIDComponent) =>
|
|
||||||
{
|
|
||||||
if (sceneIDComponent.id == sceneID)
|
|
||||||
{
|
|
||||||
entities.Add(entity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serialize each entity
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
var fileId = context.RegisterEntityForSerialization(entity);
|
|
||||||
var serializedEntity = new SerializedEntity
|
|
||||||
{
|
|
||||||
FileID = fileId,
|
|
||||||
Components = new List<SerializedComponent>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get entity location to find archetype
|
|
||||||
var locationResult = world.EntityManager.GetEntityLocation(entity);
|
|
||||||
if (locationResult.Error != Error.None)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var location = locationResult.Value;
|
|
||||||
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
|
||||||
|
|
||||||
// Serialize each component
|
|
||||||
foreach (var layout in archetype._layouts)
|
|
||||||
{
|
|
||||||
var componentType = ComponentRegistry.s_runtimeIDToType[layout.componentID];
|
|
||||||
|
|
||||||
if (componentType == null || componentType.AssemblyQualifiedName == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get component data
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
var pComponentData = archetype.GetComponentData(location.chunkIndex, location.rowIndex, layout.componentID);
|
|
||||||
if (pComponentData == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize component to JSON
|
|
||||||
// We need to box the unmanaged data to serialize it
|
|
||||||
var boxedData = System.Runtime.InteropServices.Marshal.PtrToStructure((IntPtr)pComponentData, componentType);
|
|
||||||
var componentJson = JsonSerializer.Serialize(boxedData, componentType, s_serializerOptions);
|
|
||||||
var jsonElement = JsonDocument.Parse(componentJson).RootElement;
|
|
||||||
|
|
||||||
serializedEntity.Components.Add(new SerializedComponent
|
|
||||||
{
|
|
||||||
TypeName = componentType.AssemblyQualifiedName,
|
|
||||||
Data = jsonElement
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sceneFile.Entities.Add(serializedEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to file
|
|
||||||
var json = JsonSerializer.Serialize(sceneFile, s_serializerOptions);
|
|
||||||
await File.WriteAllTextAsync(filePath, json);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads a scene from a JSON file into the specified world.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world to load the scene into.</param>
|
|
||||||
/// <param name="filePath">The path to the scene file.</param>
|
|
||||||
/// <param name="newSceneID">The new scene ID to assign to loaded entities.</param>
|
|
||||||
/// <returns>The number of entities loaded.</returns>
|
|
||||||
public static async Task<int> LoadSceneAsync(World world, string filePath, short newSceneID)
|
|
||||||
{
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException($"Scene file not found: {filePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = await File.ReadAllTextAsync(filePath);
|
|
||||||
var sceneFile = JsonSerializer.Deserialize<SceneFile>(json, s_serializerOptions);
|
|
||||||
|
|
||||||
if (sceneFile == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Failed to deserialize scene file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
using var context = SerializationContext.Create();
|
|
||||||
|
|
||||||
// Pass 1: Create all entities and build the ID mapping
|
|
||||||
var fileIdToEntity = new Dictionary<int, Entity>();
|
|
||||||
|
|
||||||
foreach (var serializedEntity in sceneFile.Entities)
|
|
||||||
{
|
|
||||||
var entity = world.EntityManager.CreateEntity();
|
|
||||||
fileIdToEntity[serializedEntity.FileID] = entity;
|
|
||||||
context.RegisterEntity(serializedEntity.FileID, entity);
|
|
||||||
|
|
||||||
// Add SceneID component
|
|
||||||
world.EntityManager.AddComponent(entity, new SceneID { id = newSceneID });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: Deserialize components (with automatic entity reference remapping)
|
|
||||||
foreach (var serializedEntity in sceneFile.Entities)
|
|
||||||
{
|
|
||||||
if (!fileIdToEntity.TryGetValue(serializedEntity.FileID, out var entity))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var serializedComponent in serializedEntity.Components)
|
|
||||||
{
|
|
||||||
var componentType = Type.GetType(serializedComponent.TypeName);
|
|
||||||
if (componentType == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip SceneID as we already added it
|
|
||||||
if (componentType == typeof(SceneID))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Deserialize the component data
|
|
||||||
var componentData = JsonSerializer.Deserialize(serializedComponent.Data.GetRawText(), componentType, s_serializerOptions);
|
|
||||||
|
|
||||||
if (componentData == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add component to entity
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
var componentID = ComponentRegistry.GetComponentID(componentType);
|
|
||||||
if (componentID.IsInvalid)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For unmanaged components, we can use pointer magic
|
|
||||||
if (componentType.IsValueType)
|
|
||||||
{
|
|
||||||
var pinnedData = System.Runtime.InteropServices.GCHandle.Alloc(componentData, System.Runtime.InteropServices.GCHandleType.Pinned);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var ptr = pinnedData.AddrOfPinnedObject().ToPointer();
|
|
||||||
world.EntityManager.AddComponent(entity, componentID, ptr);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
pinnedData.Free();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log error but continue loading other components
|
|
||||||
Console.WriteLine($"Failed to deserialize component {serializedComponent.TypeName}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileIdToEntity.Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,4 +24,12 @@
|
|||||||
<ProjectReference Include="..\Ghost.Graphics\Ghost.Graphics.csproj" />
|
<ProjectReference Include="..\Ghost.Graphics\Ghost.Graphics.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Services\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
using Ghost.Core;
|
|
||||||
using Ghost.Engine.Components;
|
|
||||||
using Ghost.Engine.Core;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
|
||||||
|
|
||||||
namespace Ghost.Engine.IO;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles binary serialization and deserialization of scenes for AOT-compatible runtime use.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Binary format provides fast, compact scene loading suitable for AOT compilation.
|
|
||||||
/// Uses direct memory copying for component data without reflection.
|
|
||||||
/// </remarks>
|
|
||||||
public static unsafe class SceneBinarySerializer
|
|
||||||
{
|
|
||||||
private const int MAGIC_NUMBER = 0x47534345; // "GSCE" (Ghost Scene)
|
|
||||||
private const int VERSION = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves a scene to a binary file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world containing the entities.</param>
|
|
||||||
/// <param name="sceneID">The scene ID to save.</param>
|
|
||||||
/// <param name="filePath">The path to save the scene file.</param>
|
|
||||||
public static void SaveScene(World world, short sceneID, string filePath)
|
|
||||||
{
|
|
||||||
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
|
|
||||||
using var writer = new BinaryWriter(stream);
|
|
||||||
using var context = SerializationContext.Create();
|
|
||||||
|
|
||||||
// Write header
|
|
||||||
writer.Write(MAGIC_NUMBER);
|
|
||||||
writer.Write(VERSION);
|
|
||||||
writer.Write(sceneID);
|
|
||||||
|
|
||||||
// Query all entities with the specified SceneID
|
|
||||||
var queryID = new QueryBuilder()
|
|
||||||
.WithAll<SceneID>()
|
|
||||||
.Build(world);
|
|
||||||
|
|
||||||
var entities = new List<Entity>();
|
|
||||||
|
|
||||||
world.ComponentManager.GetEntityQueryReference(queryID).ForEach<SceneID>((Entity entity, ref SceneID sceneIDComponent) =>
|
|
||||||
{
|
|
||||||
if (sceneIDComponent.id == sceneID)
|
|
||||||
{
|
|
||||||
entities.Add(entity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Write entity count
|
|
||||||
writer.Write(entities.Count);
|
|
||||||
|
|
||||||
// Allocate buffer for zero-filled component data (reused across loop iterations)
|
|
||||||
const int MaxComponentSize = 4096; // Reasonable max size for most components
|
|
||||||
var zeroBuffer = stackalloc byte[MaxComponentSize];
|
|
||||||
MemoryUtility.MemSet(zeroBuffer, 0, MaxComponentSize);
|
|
||||||
|
|
||||||
// Write each entity
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
var fileId = context.RegisterEntityForSerialization(entity);
|
|
||||||
|
|
||||||
// Write entity file ID
|
|
||||||
writer.Write(fileId);
|
|
||||||
|
|
||||||
// Get entity location
|
|
||||||
var locationResult = world.EntityManager.GetEntityLocation(entity);
|
|
||||||
if (locationResult.Error != Error.None)
|
|
||||||
{
|
|
||||||
// Write 0 components for invalid entity
|
|
||||||
writer.Write(0);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var location = locationResult.Value;
|
|
||||||
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
|
||||||
|
|
||||||
// Write component count
|
|
||||||
writer.Write(archetype._layouts.Count);
|
|
||||||
|
|
||||||
// Write each component
|
|
||||||
foreach (var layout in archetype._layouts)
|
|
||||||
{
|
|
||||||
// Write component type ID
|
|
||||||
writer.Write((int)layout.componentID);
|
|
||||||
|
|
||||||
// Write component size
|
|
||||||
writer.Write(layout.size);
|
|
||||||
|
|
||||||
// Get component data pointer
|
|
||||||
var pComponentData = archetype.GetComponentData(location.chunkIndex, location.rowIndex, layout.componentID);
|
|
||||||
if (pComponentData == null)
|
|
||||||
{
|
|
||||||
// Write zero-filled data if component not found
|
|
||||||
if (layout.size > MaxComponentSize)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Component size {layout.size} exceeds maximum buffer size {MaxComponentSize}");
|
|
||||||
}
|
|
||||||
writer.Write(new ReadOnlySpan<byte>(zeroBuffer, layout.size));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Write component data directly
|
|
||||||
writer.Write(new ReadOnlySpan<byte>(pComponentData, layout.size));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads a scene from a binary file into the specified world.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world to load the scene into.</param>
|
|
||||||
/// <param name="filePath">The path to the scene file.</param>
|
|
||||||
/// <param name="newSceneID">The new scene ID to assign to loaded entities.</param>
|
|
||||||
/// <returns>The number of entities loaded.</returns>
|
|
||||||
public static int LoadScene(World world, string filePath, short newSceneID)
|
|
||||||
{
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException($"Scene file not found: {filePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
|
||||||
using var reader = new BinaryReader(stream);
|
|
||||||
using var context = SerializationContext.Create();
|
|
||||||
|
|
||||||
// Read and validate header
|
|
||||||
var magic = reader.ReadInt32();
|
|
||||||
if (magic != MAGIC_NUMBER)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Invalid scene file format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var version = reader.ReadInt32();
|
|
||||||
if (version != VERSION)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"Unsupported scene file version: {version}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var savedSceneID = reader.ReadInt16();
|
|
||||||
|
|
||||||
// Read entity count
|
|
||||||
var entityCount = reader.ReadInt32();
|
|
||||||
|
|
||||||
// Pass 1: Create all entities and build ID mapping
|
|
||||||
var fileIdToEntity = new Dictionary<int, Entity>(entityCount);
|
|
||||||
var entityComponents = new List<(int fileId, List<(Identifier<IComponent> componentID, int size, byte[] data)> components)>(entityCount);
|
|
||||||
|
|
||||||
for (var i = 0; i < entityCount; i++)
|
|
||||||
{
|
|
||||||
var fileId = reader.ReadInt32();
|
|
||||||
var componentCount = reader.ReadInt32();
|
|
||||||
|
|
||||||
var components = new List<(Identifier<IComponent> componentID, int size, byte[] data)>(componentCount);
|
|
||||||
|
|
||||||
// Read component data
|
|
||||||
for (var j = 0; j < componentCount; j++)
|
|
||||||
{
|
|
||||||
var componentID = new Identifier<IComponent>(reader.ReadInt32());
|
|
||||||
var size = reader.ReadInt32();
|
|
||||||
var data = reader.ReadBytes(size);
|
|
||||||
|
|
||||||
components.Add((componentID, size, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
entityComponents.Add((fileId, components));
|
|
||||||
|
|
||||||
// Create entity
|
|
||||||
var entity = world.EntityManager.CreateEntity();
|
|
||||||
fileIdToEntity[fileId] = entity;
|
|
||||||
context.RegisterEntity(fileId, entity);
|
|
||||||
|
|
||||||
// Add SceneID component
|
|
||||||
world.EntityManager.AddComponent(entity, new SceneID { id = newSceneID });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: Add components to entities (with automatic entity reference remapping)
|
|
||||||
foreach (var (fileId, components) in entityComponents)
|
|
||||||
{
|
|
||||||
if (!fileIdToEntity.TryGetValue(fileId, out var entity))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (componentID, size, data) in components)
|
|
||||||
{
|
|
||||||
// Skip SceneID as we already added it
|
|
||||||
if (componentID == ComponentTypeID<SceneID>.Value)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed (byte* pData = data)
|
|
||||||
{
|
|
||||||
// Remap Entity references in the component data
|
|
||||||
RemapEntityReferences(pData, componentID, context);
|
|
||||||
|
|
||||||
// Add component
|
|
||||||
world.EntityManager.AddComponent(entity, componentID, pData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileIdToEntity.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remaps Entity references within component data.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This is a simple implementation that checks if the component contains Entity fields.
|
|
||||||
/// For Hierarchy, it remaps parent, firstChild, and nextSibling fields.
|
|
||||||
/// </remarks>
|
|
||||||
private static void RemapEntityReferences(byte* pComponentData, Identifier<IComponent> componentID, SerializationContext context)
|
|
||||||
{
|
|
||||||
// Check if this is the Hierarchy component
|
|
||||||
if (componentID == ComponentTypeID<Hierarchy>.Value)
|
|
||||||
{
|
|
||||||
var hierarchy = (Hierarchy*)pComponentData;
|
|
||||||
|
|
||||||
// Remap parent
|
|
||||||
if (hierarchy->parent.IsValid && context.TryGetFileId(hierarchy->parent, out var parentFileId))
|
|
||||||
{
|
|
||||||
if (context.TryGetEntity(parentFileId, out var newParent))
|
|
||||||
{
|
|
||||||
hierarchy->parent = newParent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remap firstChild
|
|
||||||
if (hierarchy->firstChild.IsValid && context.TryGetFileId(hierarchy->firstChild, out var firstChildFileId))
|
|
||||||
{
|
|
||||||
if (context.TryGetEntity(firstChildFileId, out var newFirstChild))
|
|
||||||
{
|
|
||||||
hierarchy->firstChild = newFirstChild;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remap nextSibling
|
|
||||||
if (hierarchy->nextSibling.IsValid && context.TryGetFileId(hierarchy->nextSibling, out var nextSiblingFileId))
|
|
||||||
{
|
|
||||||
if (context.TryGetEntity(nextSiblingFileId, out var newNextSibling))
|
|
||||||
{
|
|
||||||
hierarchy->nextSibling = newNextSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add remapping for other components with Entity fields
|
|
||||||
// This could be automated using source generators in the future
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
using Ghost.Entities;
|
|
||||||
|
|
||||||
namespace Ghost.Engine.IO;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides a thread-safe context for Entity ID remapping during deserialization.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This class manages the mapping between file-local entity IDs (0, 1, 2...)
|
|
||||||
/// and runtime entity IDs during scene deserialization. The context is scoped
|
|
||||||
/// to the current async operation using AsyncLocal storage.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class SerializationContext : IDisposable
|
|
||||||
{
|
|
||||||
private static readonly AsyncLocal<SerializationContext?> s_current = new();
|
|
||||||
|
|
||||||
private readonly Dictionary<int, Entity> _fileIdToEntity = new();
|
|
||||||
private readonly Dictionary<Entity, int> _entityToFileId = new();
|
|
||||||
private int _nextFileId = 0;
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current serialization context for this async operation.
|
|
||||||
/// </summary>
|
|
||||||
public static SerializationContext? Current => s_current.Value;
|
|
||||||
|
|
||||||
private SerializationContext()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates and activates a new serialization context for the current async scope.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new serialization context. Must be disposed when done.</returns>
|
|
||||||
public static SerializationContext Create()
|
|
||||||
{
|
|
||||||
if (s_current.Value != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("A serialization context is already active in this scope.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var context = new SerializationContext();
|
|
||||||
s_current.Value = context;
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers an entity mapping for deserialization.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileId">The file-local entity ID.</param>
|
|
||||||
/// <param name="runtimeEntity">The runtime entity.</param>
|
|
||||||
public void RegisterEntity(int fileId, Entity runtimeEntity)
|
|
||||||
{
|
|
||||||
_fileIdToEntity[fileId] = runtimeEntity;
|
|
||||||
_entityToFileId[runtimeEntity] = fileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a runtime entity and assigns it the next available file ID for serialization.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="runtimeEntity">The runtime entity to register.</param>
|
|
||||||
/// <returns>The assigned file-local ID.</returns>
|
|
||||||
public int RegisterEntityForSerialization(Entity runtimeEntity)
|
|
||||||
{
|
|
||||||
if (!_entityToFileId.TryGetValue(runtimeEntity, out var fileId))
|
|
||||||
{
|
|
||||||
fileId = _nextFileId++;
|
|
||||||
_entityToFileId[runtimeEntity] = fileId;
|
|
||||||
_fileIdToEntity[fileId] = runtimeEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to get the runtime entity for a file-local ID.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileId">The file-local entity ID.</param>
|
|
||||||
/// <param name="entity">The runtime entity if found.</param>
|
|
||||||
/// <returns>True if the entity was found, false otherwise.</returns>
|
|
||||||
public bool TryGetEntity(int fileId, out Entity entity)
|
|
||||||
{
|
|
||||||
return _fileIdToEntity.TryGetValue(fileId, out entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to get the file-local ID for a runtime entity.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity">The runtime entity.</param>
|
|
||||||
/// <param name="fileId">The file-local ID if found.</param>
|
|
||||||
/// <returns>True if the file ID was found, false otherwise.</returns>
|
|
||||||
public bool TryGetFileId(Entity entity, out int fileId)
|
|
||||||
{
|
|
||||||
return _entityToFileId.TryGetValue(entity, out fileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_current.Value = null;
|
|
||||||
_fileIdToEntity.Clear();
|
|
||||||
_entityToFileId.Clear();
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
using Ghost.Engine.Components;
|
|
||||||
using Ghost.Engine.Core;
|
|
||||||
using Ghost.Engine.IO;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Ghost.Engine.Services;
|
|
||||||
|
|
||||||
public enum SceneLoadMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Unloads all currently loaded scenes before loading the new scene.
|
|
||||||
/// </summary>
|
|
||||||
Single,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads the scene additively without unloading existing scenes.
|
|
||||||
/// </summary>
|
|
||||||
Additive
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages scene loading, unloading, and saving operations using binary serialization.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This runtime scene manager uses binary serialization for AOT compatibility.
|
|
||||||
/// For editor JSON serialization, use EditorSceneManager in Ghost.Editor.Core.
|
|
||||||
/// </remarks>
|
|
||||||
public static class SceneManager
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<short, Scene> s_loadedScenes = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all currently loaded scenes.
|
|
||||||
/// </summary>
|
|
||||||
public static IReadOnlyCollection<Scene> LoadedScenes => s_loadedScenes.Values;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads a scene from a binary file into the specified world.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world to load the scene into.</param>
|
|
||||||
/// <param name="filePath">The path to the scene file.</param>
|
|
||||||
/// <param name="loadMode">The load mode (Single or Additive).</param>
|
|
||||||
/// <returns>The loaded scene.</returns>
|
|
||||||
public static Scene LoadScene(World world, string filePath, SceneLoadMode loadMode = SceneLoadMode.Single)
|
|
||||||
{
|
|
||||||
if (loadMode == SceneLoadMode.Single)
|
|
||||||
{
|
|
||||||
// Unload all currently loaded scenes in this world
|
|
||||||
var scenesToUnload = s_loadedScenes.Values.Where(s => s.World == world).ToList();
|
|
||||||
foreach (var scene in scenesToUnload)
|
|
||||||
{
|
|
||||||
UnloadScene(scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new scene ID for this load
|
|
||||||
var sceneName = Path.GetFileNameWithoutExtension(filePath);
|
|
||||||
var newScene = new Scene(world, sceneName);
|
|
||||||
|
|
||||||
// Load the scene data using binary serialization
|
|
||||||
SceneBinarySerializer.LoadScene(world, filePath, newScene.ID);
|
|
||||||
|
|
||||||
// Register the loaded scene
|
|
||||||
s_loadedScenes[newScene.ID] = newScene;
|
|
||||||
|
|
||||||
return newScene;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves a scene to a binary file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scene">The scene to save.</param>
|
|
||||||
/// <param name="filePath">The path to save the scene file.</param>
|
|
||||||
public static void SaveScene(Scene scene, string filePath)
|
|
||||||
{
|
|
||||||
SceneBinarySerializer.SaveScene(scene.World, scene.ID, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unloads a scene, destroying all entities belonging to it.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scene">The scene to unload.</param>
|
|
||||||
public static void UnloadScene(Scene scene)
|
|
||||||
{
|
|
||||||
if (!s_loadedScenes.ContainsKey(scene.ID))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query all entities with the scene's ID
|
|
||||||
var queryID = new QueryBuilder()
|
|
||||||
.WithAll<SceneID>()
|
|
||||||
.Build(scene.World);
|
|
||||||
|
|
||||||
var entitiesToDestroy = new List<Entity>();
|
|
||||||
|
|
||||||
scene.World.ComponentManager.GetEntityQueryReference(queryID).ForEach<SceneID>((Entity entity, ref SceneID sceneIDComponent) =>
|
|
||||||
{
|
|
||||||
if (sceneIDComponent.id == scene.ID)
|
|
||||||
{
|
|
||||||
entitiesToDestroy.Add(entity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Destroy all entities in this scene
|
|
||||||
scene.World.EntityManager.DestroyEntities(CollectionsMarshal.AsSpan(entitiesToDestroy));
|
|
||||||
|
|
||||||
// Remove from loaded scenes
|
|
||||||
s_loadedScenes.Remove(scene.ID);
|
|
||||||
|
|
||||||
// Dispose the scene handle
|
|
||||||
scene.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unloads all scenes in the specified world.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world whose scenes to unload.</param>
|
|
||||||
public static void UnloadAllScenes(World world)
|
|
||||||
{
|
|
||||||
var scenesToUnload = s_loadedScenes.Values.Where(s => s.World == world).ToList();
|
|
||||||
foreach (var scene in scenesToUnload)
|
|
||||||
{
|
|
||||||
UnloadScene(scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to get a loaded scene by its ID.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneID">The scene ID to find.</param>
|
|
||||||
/// <param name="scene">The found scene, or null if not loaded.</param>
|
|
||||||
/// <returns>True if the scene was found, false otherwise.</returns>
|
|
||||||
public static bool TryGetScene(short sceneID, out Scene? scene)
|
|
||||||
{
|
|
||||||
return s_loadedScenes.TryGetValue(sceneID, out scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -510,6 +510,42 @@ public ref partial struct QueryBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void WithAll(params Span<Identifier<IComponent>> componentIDs)
|
||||||
|
{
|
||||||
|
_all.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WithAny(params Span<Identifier<IComponent>> componentIDs)
|
||||||
|
{
|
||||||
|
_any.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WithAbsent(params Span<Identifier<IComponent>> componentIDs)
|
||||||
|
{
|
||||||
|
_absent.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WithNone(params Span<Identifier<IComponent>> componentIDs)
|
||||||
|
{
|
||||||
|
_none.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WithDisabled(params Span<Identifier<IComponent>> componentIDs)
|
||||||
|
{
|
||||||
|
_disabled.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WithPresent(params Span<Identifier<IComponent>> componentIDs)
|
||||||
|
{
|
||||||
|
_present.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WithPresentRW(params Span<Identifier<IComponent>> componentIDs)
|
||||||
|
{
|
||||||
|
_present.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
_rw.AddRange(componentIDs, componentIDs.Length);
|
||||||
|
}
|
||||||
|
|
||||||
public Identifier<EntityQuery> Build(World world, Allocator allocator = Allocator.Persistent)
|
public Identifier<EntityQuery> Build(World world, Allocator allocator = Allocator.Persistent)
|
||||||
{
|
{
|
||||||
// 1. Calculate max component ID to size the BitSets
|
// 1. Calculate max component ID to size the BitSets
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ internal class MeshRenderPass : IRenderPass
|
|||||||
|
|
||||||
private void CompileBlitShader(ref readonly RenderingContext ctx)
|
private void CompileBlitShader(ref readonly RenderingContext ctx)
|
||||||
{
|
{
|
||||||
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Blit.gsdef", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
|
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Blit.gshdr", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
|
||||||
_blitShader = ctx.ResourceAllocator.CreateGraphicsShader(shaderDescriptor);
|
_blitShader = ctx.ResourceAllocator.CreateGraphicsShader(shaderDescriptor);
|
||||||
_blitMaterial = ctx.ResourceAllocator.CreateMaterial(_blitShader);
|
_blitMaterial = ctx.ResourceAllocator.CreateMaterial(_blitShader);
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ internal class MeshRenderPass : IRenderPass
|
|||||||
{
|
{
|
||||||
CompileBlitShader(in ctx);
|
CompileBlitShader(in ctx);
|
||||||
|
|
||||||
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/Ghost.Graphics/test.gsdef", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
|
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/Ghost.Graphics/test.gshdr", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
|
||||||
|
|
||||||
_shader = ctx.ResourceAllocator.CreateGraphicsShader(shaderDescriptor);
|
_shader = ctx.ResourceAllocator.CreateGraphicsShader(shaderDescriptor);
|
||||||
_material = ctx.ResourceAllocator.CreateMaterial(_shader);
|
_material = ctx.ResourceAllocator.CreateMaterial(_shader);
|
||||||
|
|||||||
Reference in New Issue
Block a user