Update scene graph
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
[*]
|
||||||
|
max_line_length = 400
|
||||||
|
|
||||||
[*.cs]
|
[*.cs]
|
||||||
csharp_new_line_before_open_brace = all
|
csharp_new_line_before_open_brace = all
|
||||||
csharp_preserve_single_line_statements = true
|
csharp_preserve_single_line_statements = true
|
||||||
@@ -8,5 +11,3 @@ dotnet_separate_import_directive_groups = false
|
|||||||
|
|
||||||
dotnet_style_prefer_collection_expression = false
|
dotnet_style_prefer_collection_expression = false
|
||||||
dotnet_style_collection_initializer = false
|
dotnet_style_collection_initializer = false
|
||||||
|
|
||||||
max_line_length = 400
|
|
||||||
@@ -129,6 +129,11 @@ public static class Logger
|
|||||||
s_logger.Log(message, level);
|
s_logger.Log(message, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Log(LogLevel level, string format, params object?[] args)
|
||||||
|
{
|
||||||
|
s_logger.Log(string.Format(format, args), level);
|
||||||
|
}
|
||||||
|
|
||||||
public static void LogInfo(object? message)
|
public static void LogInfo(object? message)
|
||||||
{
|
{
|
||||||
s_logger.Log(message?.ToString() ?? "null", LogLevel.Info);
|
s_logger.Log(message?.ToString() ?? "null", LogLevel.Info);
|
||||||
@@ -139,6 +144,11 @@ public static class Logger
|
|||||||
s_logger.Log(message, LogLevel.Info);
|
s_logger.Log(message, LogLevel.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void LogInfo(string format, params object?[] args)
|
||||||
|
{
|
||||||
|
s_logger.Log(string.Format(format, args), LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
public static void LogWarning(object? message)
|
public static void LogWarning(object? message)
|
||||||
{
|
{
|
||||||
s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning);
|
s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning);
|
||||||
@@ -149,6 +159,11 @@ public static class Logger
|
|||||||
s_logger.Log(message, LogLevel.Warning);
|
s_logger.Log(message, LogLevel.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void LogWarning(string format, params object?[] args)
|
||||||
|
{
|
||||||
|
s_logger.Log(string.Format(format, args), LogLevel.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
public static void LogError(object? message)
|
public static void LogError(object? message)
|
||||||
{
|
{
|
||||||
s_logger.Log(message?.ToString() ?? "null", LogLevel.Error);
|
s_logger.Log(message?.ToString() ?? "null", LogLevel.Error);
|
||||||
@@ -159,6 +174,11 @@ public static class Logger
|
|||||||
s_logger.Log(message, LogLevel.Error);
|
s_logger.Log(message, LogLevel.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void LogError(string format, params object?[] args)
|
||||||
|
{
|
||||||
|
s_logger.Log(string.Format(format, args), LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
public static void LogError(Exception ex)
|
public static void LogError(Exception ex)
|
||||||
{
|
{
|
||||||
s_logger.Log(ex);
|
s_logger.Log(ex);
|
||||||
|
|||||||
22
Ghost.Editor.Core/AssetHandle/Asset.cs
Normal file
22
Ghost.Editor.Core/AssetHandle/Asset.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Ghost.Editor.Core.AssetHandle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The base class for all asset types in the Ghost Editor.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class Asset
|
||||||
|
{
|
||||||
|
public abstract string Name
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid ID
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Asset(Guid id)
|
||||||
|
{
|
||||||
|
ID = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ namespace Ghost.Editor.Core.AssetHandle;
|
|||||||
public static partial class AssetDatabase
|
public static partial class AssetDatabase
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, Type> s_importerTypeLookup = new();
|
private static readonly Dictionary<string, Type> s_importerTypeLookup = new();
|
||||||
|
private static readonly Dictionary<Guid, string> s_assetPathLookup = new();
|
||||||
|
private static readonly Dictionary<string, Guid> s_pathAssetLookup = new();
|
||||||
|
|
||||||
private static void InitializeMetaData()
|
private static void InitializeMetaData()
|
||||||
{
|
{
|
||||||
@@ -38,12 +40,12 @@ public static partial class AssetDatabase
|
|||||||
return Error.NotFound;
|
return Error.NotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Path.GetExtension(assetPath).Equals(".meta", StringComparison.OrdinalIgnoreCase))
|
if (Path.GetExtension(assetPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return Error.InvalidState;
|
return Error.InvalidState;
|
||||||
}
|
}
|
||||||
|
|
||||||
return assetPath + ".meta";
|
return assetPath + FileExtensions.META_FILE_EXTENSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath)
|
private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath)
|
||||||
@@ -64,37 +66,46 @@ public static partial class AssetDatabase
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteMetaFile(string metaFilePath, AssetMeta metaData)
|
private static async Task<Result> WriteMetaFileAsync(string metaFilePath, AssetMeta metaData)
|
||||||
{
|
{
|
||||||
using var fileStream = File.Create(metaFilePath);
|
using var fileStream = File.Create(metaFilePath);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
JsonSerializer.Serialize(fileStream, metaData);
|
await JsonSerializer.SerializeAsync(fileStream, metaData);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex);
|
return Result.Failure(ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Result.Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Result GenerateMetaFile(string assetPath)
|
internal static async Task<Result> GenerateMetaFileAsync(string assetPath)
|
||||||
{
|
{
|
||||||
|
Result r;
|
||||||
|
|
||||||
var metaFileResult = GetMetaFilePath(assetPath);
|
var metaFileResult = GetMetaFilePath(assetPath);
|
||||||
if (!metaFileResult.IsSuccess)
|
if (metaFileResult.IsFailure)
|
||||||
{
|
{
|
||||||
return Result.Failure(metaFileResult.Error.ToString());
|
return Result.Failure(metaFileResult.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(metaFileResult.Value))
|
if (File.Exists(metaFileResult.Value))
|
||||||
{
|
{
|
||||||
var existingMeta = JsonSerializer.Deserialize<AssetMeta>(File.ReadAllText(metaFileResult.Value));
|
using var fileStream = File.OpenRead(metaFileResult.Value);
|
||||||
|
var existingMeta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream);
|
||||||
if (existingMeta != null && s_assetPathLookup.TryGetValue(existingMeta.Guid, out var path))
|
if (existingMeta != null && s_assetPathLookup.TryGetValue(existingMeta.Guid, out var path))
|
||||||
{
|
{
|
||||||
if (assetPath != path)
|
if (assetPath != path)
|
||||||
{
|
{
|
||||||
existingMeta.Guid = Guid.NewGuid();
|
existingMeta.Guid = Guid.NewGuid();
|
||||||
WriteMetaFile(metaFileResult.Value, existingMeta);
|
r = await WriteMetaFileAsync(metaFileResult.Value, existingMeta);
|
||||||
|
if (r.IsFailure)
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,14 +119,14 @@ public static partial class AssetDatabase
|
|||||||
Settings = defaultSettings
|
Settings = defaultSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
WriteMetaFile(metaFileResult.Value, metaData);
|
r = await WriteMetaFileAsync(metaFileResult.Value, metaData);
|
||||||
|
|
||||||
return Result.Success();
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnAssetCreated(object sender, FileSystemEventArgs e)
|
private static async void OnAssetCreated(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
GenerateMetaFile(e.FullPath);
|
await GenerateMetaFileAsync(e.FullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnAssetDeleted(object sender, FileSystemEventArgs e)
|
private static void OnAssetDeleted(object sender, FileSystemEventArgs e)
|
||||||
@@ -142,10 +153,10 @@ public static partial class AssetDatabase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnAssetRenamed(object sender, RenamedEventArgs e)
|
private static async void OnAssetRenamed(object sender, RenamedEventArgs e)
|
||||||
{
|
{
|
||||||
var oldMetaPath = e.OldFullPath + ".meta";
|
var oldMetaPath = e.OldFullPath + FileExtensions.META_FILE_EXTENSION;
|
||||||
var newMetaPath = e.FullPath + ".meta";
|
var newMetaPath = e.FullPath + FileExtensions.META_FILE_EXTENSION;
|
||||||
|
|
||||||
if (File.Exists(oldMetaPath))
|
if (File.Exists(oldMetaPath))
|
||||||
{
|
{
|
||||||
@@ -153,7 +164,7 @@ public static partial class AssetDatabase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
GenerateMetaFile(e.FullPath);
|
await GenerateMetaFileAsync(e.FullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Ghost.Core;
|
||||||
using Ghost.Editor.Core.Utilities;
|
using Ghost.Editor.Core.Utilities;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -24,7 +25,7 @@ public static partial class AssetDatabase
|
|||||||
{
|
{
|
||||||
if (_assetOpenHandlers.ContainsKey(ext))
|
if (_assetOpenHandlers.ContainsKey(ext))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Duplicate handler for extension '{ext}'");
|
Logger.LogError($"Duplicate asset open handler for extension '{ext}' found in method '{method.Name}'. Existing handler will be overwritten.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_assetOpenHandlers[ext] = del;
|
_assetOpenHandlers[ext] = del;
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ public static partial class AssetDatabase
|
|||||||
{
|
{
|
||||||
private static FileSystemWatcher? s_watcher;
|
private static FileSystemWatcher? s_watcher;
|
||||||
|
|
||||||
private static readonly Dictionary<Guid, string> s_assetPathLookup = new();
|
|
||||||
|
|
||||||
public static DirectoryInfo? AssetsDirectory
|
public static DirectoryInfo? AssetsDirectory
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ internal class AssetMeta
|
|||||||
public Guid Guid
|
public Guid Guid
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
internal set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImporterSettings? Settings
|
public ImporterSettings? Settings
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
using Ghost.Engine.Components;
|
|
||||||
using Ghost.Engine.Core;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages the editor world and scene graph hierarchy.
|
|
||||||
/// Provides functionality to load/unload scenes and maintain the editor-side scene graph.
|
|
||||||
/// </summary>
|
|
||||||
public class EditorWorldManager
|
|
||||||
{
|
|
||||||
private readonly World _editorWorld;
|
|
||||||
private readonly SceneManager _sceneManager;
|
|
||||||
private readonly ObservableCollection<SceneNode> _loadedScenes;
|
|
||||||
private readonly Dictionary<short, SceneNode> _sceneIdToNode;
|
|
||||||
private readonly Dictionary<Entity, EntityNode> _entityToNode;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the editor world instance.
|
|
||||||
/// </summary>
|
|
||||||
public World EditorWorld => _editorWorld;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the runtime scene manager.
|
|
||||||
/// </summary>
|
|
||||||
public SceneManager SceneManager => _sceneManager;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the collection of loaded scenes in the editor.
|
|
||||||
/// </summary>
|
|
||||||
public ReadOnlyObservableCollection<SceneNode> LoadedScenes { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when a scene is loaded.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<SceneNode>? OnSceneLoaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when a scene is unloaded.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<SceneNode>? OnSceneUnloaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when an entity node is created.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<EntityNode>? OnEntityNodeCreated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when an entity node is destroyed.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<EntityNode>? OnEntityNodeDestroyed;
|
|
||||||
|
|
||||||
public EditorWorldManager(World editorWorld, SceneManager sceneManager)
|
|
||||||
{
|
|
||||||
_editorWorld = editorWorld;
|
|
||||||
_sceneManager = sceneManager;
|
|
||||||
_loadedScenes = [];
|
|
||||||
_sceneIdToNode = [];
|
|
||||||
_entityToNode = [];
|
|
||||||
|
|
||||||
LoadedScenes = new ReadOnlyObservableCollection<SceneNode>(_loadedScenes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new empty scene in the editor.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The name of the scene.</param>
|
|
||||||
/// <returns>The created scene node.</returns>
|
|
||||||
public SceneNode CreateNewScene(string name = "New Scene")
|
|
||||||
{
|
|
||||||
var runtimeScene = _sceneManager.CreateScene();
|
|
||||||
var sceneNode = new SceneNode(runtimeScene, name)
|
|
||||||
{
|
|
||||||
IsLoaded = true
|
|
||||||
};
|
|
||||||
|
|
||||||
_loadedScenes.Add(sceneNode);
|
|
||||||
_sceneIdToNode[runtimeScene.ID] = sceneNode;
|
|
||||||
|
|
||||||
OnSceneLoaded?.Invoke(sceneNode);
|
|
||||||
|
|
||||||
return sceneNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unloads a scene from the editor.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneNode">The scene to unload.</param>
|
|
||||||
public void UnloadScene(SceneNode sceneNode)
|
|
||||||
{
|
|
||||||
if (!_loadedScenes.Contains(sceneNode))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all entity nodes from tracking
|
|
||||||
foreach (var entityNode in sceneNode.GetAllEntities().ToList())
|
|
||||||
{
|
|
||||||
_entityToNode.Remove(entityNode.Entity);
|
|
||||||
OnEntityNodeDestroyed?.Invoke(entityNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unload runtime scene
|
|
||||||
_sceneManager.UnloadScene(sceneNode.Scene);
|
|
||||||
|
|
||||||
// Remove from loaded scenes
|
|
||||||
_loadedScenes.Remove(sceneNode);
|
|
||||||
_sceneIdToNode.Remove(sceneNode.Scene.ID);
|
|
||||||
|
|
||||||
sceneNode.IsLoaded = false;
|
|
||||||
|
|
||||||
OnSceneUnloaded?.Invoke(sceneNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an entity in the specified scene.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneNode">The scene to create the entity in.</param>
|
|
||||||
/// <param name="name">The display name of the entity.</param>
|
|
||||||
/// <param name="parent">Optional parent entity node.</param>
|
|
||||||
/// <returns>The created entity node.</returns>
|
|
||||||
public EntityNode CreateEntity(SceneNode sceneNode, string name = "Entity", EntityNode? parent = null)
|
|
||||||
{
|
|
||||||
// Create runtime entity with SceneID component
|
|
||||||
var entity = _editorWorld.EntityManager.CreateEntity();
|
|
||||||
_editorWorld.EntityManager.AddComponent(entity, new SceneID { id = sceneNode.Scene.ID });
|
|
||||||
|
|
||||||
// Create entity node
|
|
||||||
var entityNode = new EntityNode(entity, name);
|
|
||||||
|
|
||||||
// Add to scene graph
|
|
||||||
if (parent != null)
|
|
||||||
{
|
|
||||||
parent.AddChild(entityNode);
|
|
||||||
|
|
||||||
// Add Hierarchy component
|
|
||||||
_editorWorld.EntityManager.AddComponent(entity, new Hierarchy
|
|
||||||
{
|
|
||||||
parent = parent.Entity,
|
|
||||||
firstChild = Entity.Invalid,
|
|
||||||
nextSibling = Entity.Invalid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sceneNode.AddRootEntity(entityNode);
|
|
||||||
|
|
||||||
// Add root hierarchy component
|
|
||||||
_editorWorld.EntityManager.AddComponent(entity, Hierarchy.Root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track entity node
|
|
||||||
_entityToNode[entity] = entityNode;
|
|
||||||
|
|
||||||
OnEntityNodeCreated?.Invoke(entityNode);
|
|
||||||
|
|
||||||
return entityNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Destroys an entity and its node from the scene.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entityNode">The entity node to destroy.</param>
|
|
||||||
public void DestroyEntity(EntityNode entityNode)
|
|
||||||
{
|
|
||||||
// Remove from parent or scene root
|
|
||||||
if (entityNode.Parent != null)
|
|
||||||
{
|
|
||||||
entityNode.Parent.RemoveChild(entityNode);
|
|
||||||
}
|
|
||||||
else if (entityNode.OwnerScene != null)
|
|
||||||
{
|
|
||||||
entityNode.OwnerScene.RemoveRootEntity(entityNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy all children recursively
|
|
||||||
var childrenCopy = entityNode.Children.ToList();
|
|
||||||
foreach (var child in childrenCopy)
|
|
||||||
{
|
|
||||||
DestroyEntity(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy runtime entity
|
|
||||||
_editorWorld.EntityManager.DestroyEntity(entityNode.Entity);
|
|
||||||
|
|
||||||
// Remove from tracking
|
|
||||||
_entityToNode.Remove(entityNode.Entity);
|
|
||||||
|
|
||||||
OnEntityNodeDestroyed?.Invoke(entityNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the scene node for a runtime scene ID.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneId">The scene ID.</param>
|
|
||||||
/// <returns>The scene node if found, null otherwise.</returns>
|
|
||||||
public SceneNode? GetSceneNode(short sceneId)
|
|
||||||
{
|
|
||||||
_sceneIdToNode.TryGetValue(sceneId, out var sceneNode);
|
|
||||||
return sceneNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the entity node for a runtime entity.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity">The entity.</param>
|
|
||||||
/// <returns>The entity node if found, null otherwise.</returns>
|
|
||||||
public EntityNode? GetEntityNode(Entity entity)
|
|
||||||
{
|
|
||||||
_entityToNode.TryGetValue(entity, out var entityNode);
|
|
||||||
return entityNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rebuilds the scene graph from the current world state.
|
|
||||||
/// Useful after loading a scene from disk.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneNode">The scene node to rebuild.</param>
|
|
||||||
public void RebuildSceneGraph(SceneNode sceneNode)
|
|
||||||
{
|
|
||||||
// Clear existing nodes
|
|
||||||
sceneNode.RootEntities.Clear();
|
|
||||||
|
|
||||||
// Build query for entities in this scene
|
|
||||||
var builder = new QueryBuilder();
|
|
||||||
builder.WithAll([ComponentTypeID<SceneID>.Value, ComponentTypeID<Hierarchy>.Value]);
|
|
||||||
var queryID = builder.Build(_editorWorld);
|
|
||||||
ref var query = ref _editorWorld.ComponentManager.GetEntityQueryReference(queryID);
|
|
||||||
|
|
||||||
// First pass: Create all entity nodes
|
|
||||||
var entityNodes = new Dictionary<Entity, EntityNode>();
|
|
||||||
|
|
||||||
foreach (var chunk in query.GetChunkIterator())
|
|
||||||
{
|
|
||||||
var entities = chunk.GetEntities();
|
|
||||||
var sceneIDs = chunk.GetComponentData<SceneID>();
|
|
||||||
|
|
||||||
for (int i = 0; i < chunk.Count; i++)
|
|
||||||
{
|
|
||||||
if (sceneIDs[i].id == sceneNode.Scene.ID)
|
|
||||||
{
|
|
||||||
var entity = entities[i];
|
|
||||||
|
|
||||||
// Try to get existing node name or use default
|
|
||||||
var name = _entityToNode.TryGetValue(entity, out var existing)
|
|
||||||
? existing.Name
|
|
||||||
: "Entity";
|
|
||||||
|
|
||||||
var entityNode = new EntityNode(entity, name);
|
|
||||||
entityNodes[entity] = entityNode;
|
|
||||||
_entityToNode[entity] = entityNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: Build hierarchy
|
|
||||||
foreach (var chunk in query.GetChunkIterator())
|
|
||||||
{
|
|
||||||
var entities = chunk.GetEntities();
|
|
||||||
var sceneIDs = chunk.GetComponentData<SceneID>();
|
|
||||||
var hierarchies = chunk.GetComponentData<Hierarchy>();
|
|
||||||
|
|
||||||
for (int i = 0; i < chunk.Count; i++)
|
|
||||||
{
|
|
||||||
if (sceneIDs[i].id == sceneNode.Scene.ID)
|
|
||||||
{
|
|
||||||
var entity = entities[i];
|
|
||||||
var hierarchy = hierarchies[i];
|
|
||||||
var entityNode = entityNodes[entity];
|
|
||||||
|
|
||||||
if (hierarchy.parent.IsValid && entityNodes.TryGetValue(hierarchy.parent, out var parentNode))
|
|
||||||
{
|
|
||||||
parentNode.AddChild(entityNode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sceneNode.AddRootEntity(entityNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +1,10 @@
|
|||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
namespace Ghost.Editor.Core.SceneGraph;
|
||||||
|
|
||||||
/// <summary>
|
public sealed partial class EntityNode : SceneGraphNode
|
||||||
/// Represents an entity node in the editor scene graph hierarchy.
|
|
||||||
/// Contains editor-only metadata like display name and hierarchy information.
|
|
||||||
/// </summary>
|
|
||||||
public class EntityNode
|
|
||||||
{
|
{
|
||||||
private string _name;
|
private readonly Entity _entity;
|
||||||
private readonly ObservableCollection<EntityNode> _children;
|
|
||||||
|
|
||||||
/// <summary>
|
public Entity Entity => _entity;
|
||||||
/// Gets or sets the entity this node represents.
|
|
||||||
/// </summary>
|
|
||||||
public Entity Entity { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the display name for this entity in the editor.
|
|
||||||
/// This is NOT stored in runtime components.
|
|
||||||
/// </summary>
|
|
||||||
public string Name
|
|
||||||
{
|
|
||||||
get => _name;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_name = value;
|
|
||||||
OnNameChanged?.Invoke(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the parent node of this entity node.
|
|
||||||
/// </summary>
|
|
||||||
public EntityNode? Parent { get; internal set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the scene node that contains this entity.
|
|
||||||
/// </summary>
|
|
||||||
public SceneNode? OwnerScene { get; internal set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the collection of child entity nodes.
|
|
||||||
/// </summary>
|
|
||||||
public ObservableCollection<EntityNode> Children => _children;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when the name property changes.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<EntityNode>? OnNameChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when children collection changes.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<EntityNode>? OnChildrenChanged;
|
|
||||||
|
|
||||||
public EntityNode(Entity entity, string name = "Entity")
|
|
||||||
{
|
|
||||||
Entity = entity;
|
|
||||||
_name = name;
|
|
||||||
_children = [];
|
|
||||||
_children.CollectionChanged += (s, e) => OnChildrenChanged?.Invoke(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a child entity node to this node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="child">The child node to add.</param>
|
|
||||||
public void AddChild(EntityNode child)
|
|
||||||
{
|
|
||||||
if (child.Parent != null)
|
|
||||||
{
|
|
||||||
child.Parent.RemoveChild(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
child.Parent = this;
|
|
||||||
child.OwnerScene = OwnerScene;
|
|
||||||
_children.Add(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a child entity node from this node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="child">The child node to remove.</param>
|
|
||||||
/// <returns>True if the child was removed, false otherwise.</returns>
|
|
||||||
public bool RemoveChild(EntityNode child)
|
|
||||||
{
|
|
||||||
if (_children.Remove(child))
|
|
||||||
{
|
|
||||||
child.Parent = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all descendants of this node (children, grandchildren, etc.) in depth-first order.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An enumerable of all descendant nodes.</returns>
|
|
||||||
public IEnumerable<EntityNode> GetAllDescendants()
|
|
||||||
{
|
|
||||||
foreach (var child in _children)
|
|
||||||
{
|
|
||||||
yield return child;
|
|
||||||
|
|
||||||
foreach (var descendant in child.GetAllDescendants())
|
|
||||||
{
|
|
||||||
yield return descendant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds an entity node by its entity reference.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity">The entity to search for.</param>
|
|
||||||
/// <returns>The entity node if found, null otherwise.</returns>
|
|
||||||
public EntityNode? FindNode(Entity entity)
|
|
||||||
{
|
|
||||||
if (Entity.Equals(entity))
|
|
||||||
{
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in _children)
|
|
||||||
{
|
|
||||||
var found = child.FindNode(entity);
|
|
||||||
if (found != null)
|
|
||||||
{
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{Name} (Entity: {Entity})";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
# Scene Graph System Implementation Summary
|
|
||||||
|
|
||||||
All planned features from the `SceneGraph Plan.md` have been implemented successfully. Here's what was created:
|
|
||||||
|
|
||||||
## 1. Runtime Types (Ghost.Engine)
|
|
||||||
|
|
||||||
### Scene.cs
|
|
||||||
- **Scene struct**: Lightweight runtime identifier for scenes
|
|
||||||
- **SceneManager class**: Manages scene lifecycle in a world
|
|
||||||
- `CreateScene()`: Creates new scenes
|
|
||||||
- `UnloadScene()`: Destroys all entities in a scene
|
|
||||||
- `GetSceneEntities()`: Queries entities by scene ID
|
|
||||||
|
|
||||||
Key design: Minimal runtime footprint, no metadata stored.
|
|
||||||
|
|
||||||
## 2. Editor Data Structures (Ghost.Editor.Core)
|
|
||||||
|
|
||||||
### SceneNode.cs
|
|
||||||
- Editor-only scene representation
|
|
||||||
- Properties:
|
|
||||||
- `Name`: Display name (NOT in runtime)
|
|
||||||
- `FilePath`: Where scene is saved
|
|
||||||
- `IsLoaded`, `IsDirty`: Editor state tracking
|
|
||||||
- `RootEntities`: Observable collection of root entity nodes
|
|
||||||
- Methods for managing root entities and finding nodes
|
|
||||||
|
|
||||||
### EntityNode.cs
|
|
||||||
- Editor-only entity representation
|
|
||||||
- Properties:
|
|
||||||
- `Name`: Display name (NOT in runtime)
|
|
||||||
- `Parent`, `Children`: Editor hierarchy
|
|
||||||
- `OwnerScene`: Back-reference to scene
|
|
||||||
- Methods for hierarchy management (AddChild, RemoveChild, FindNode, GetAllDescendants)
|
|
||||||
|
|
||||||
Both classes use `ObservableCollection` for WinUI 3 TreeView binding support.
|
|
||||||
|
|
||||||
## 3. Editor World Management
|
|
||||||
|
|
||||||
### EditorWorldManager.cs
|
|
||||||
- Central manager for editor world and scene graph
|
|
||||||
- Features:
|
|
||||||
- Scene creation/unloading
|
|
||||||
- Entity creation/destruction with automatic SceneID assignment
|
|
||||||
- Hierarchy component management
|
|
||||||
- Entity/Scene node tracking via dictionaries
|
|
||||||
- `RebuildSceneGraph()`: Reconstructs scene graph from world state after loading
|
|
||||||
|
|
||||||
Maintains bidirectional mapping: Entity ↔ EntityNode, SceneID ↔ SceneNode
|
|
||||||
|
|
||||||
## 4. Serialization (Ghost.Editor.Core)
|
|
||||||
|
|
||||||
### SceneData.cs
|
|
||||||
JSON data structures:
|
|
||||||
- `SceneData`: Contains name + list of entities
|
|
||||||
- `EntityData`: Name, parent local ID, components dictionary
|
|
||||||
- `ComponentData`: Type name + fields dictionary
|
|
||||||
|
|
||||||
**Key Feature**: Entities are ordered by file-local ID (index in list).
|
|
||||||
|
|
||||||
### SceneSerializer.cs
|
|
||||||
Complete save/load implementation with entity reference remapping:
|
|
||||||
|
|
||||||
**Save Process**:
|
|
||||||
1. Build `Entity → FileLocalID` mapping
|
|
||||||
2. Serialize each entity's components
|
|
||||||
3. **Entity references converted to file-local IDs**
|
|
||||||
4. Validate no cross-scene references (throws exception if found)
|
|
||||||
5. Write JSON to file
|
|
||||||
|
|
||||||
**Load Process**:
|
|
||||||
1. Read JSON
|
|
||||||
2. Create all entities first (two-pass)
|
|
||||||
3. Build `FileLocalID → Entity` mapping
|
|
||||||
4. Deserialize components, **remapping file-local IDs back to global Entity IDs**
|
|
||||||
5. Rebuild hierarchy
|
|
||||||
|
|
||||||
Uses reflection to serialize/deserialize component fields. Entity references are detected by field type and specially handled.
|
|
||||||
|
|
||||||
## 5. Hierarchy System (Ghost.Engine)
|
|
||||||
|
|
||||||
### HierarchyUtility.cs
|
|
||||||
Runtime utilities for working with Hierarchy component:
|
|
||||||
- `SetParent()`: Update parent-child relationships, maintains sibling lists
|
|
||||||
- `GetParent()`: Query parent
|
|
||||||
- `GetChildren()`: Get immediate children
|
|
||||||
- `GetDescendants()`: Recursive depth-first traversal
|
|
||||||
- `IsAncestor()`: Check ancestor relationship
|
|
||||||
|
|
||||||
Uses the existing `Hierarchy` component (parent, firstChild, nextSibling pattern).
|
|
||||||
|
|
||||||
## 6. Validation (Ghost.Editor.Core)
|
|
||||||
|
|
||||||
### SceneValidator.cs
|
|
||||||
Validates scene integrity:
|
|
||||||
|
|
||||||
**ValidateSceneReferences**:
|
|
||||||
- Checks all Entity fields in components
|
|
||||||
- Ensures referenced entities exist
|
|
||||||
- **Enforces same-scene constraint** (errors on cross-scene refs)
|
|
||||||
|
|
||||||
**ValidateHierarchy**:
|
|
||||||
- Detects circular parent-child loops
|
|
||||||
- Warns if parent is outside scene
|
|
||||||
|
|
||||||
Returns `ValidationResult` with errors/warnings lists.
|
|
||||||
|
|
||||||
## Architecture Highlights
|
|
||||||
|
|
||||||
### Runtime vs Editor Separation
|
|
||||||
- Runtime (`Ghost.Engine`): Only SceneID component, SceneManager, HierarchyUtility
|
|
||||||
- Editor (`Ghost.Editor.Core`): All metadata (names), scene graph tree, serialization
|
|
||||||
- **Zero runtime overhead** for editor-only data
|
|
||||||
|
|
||||||
### Entity Reference Remapping
|
|
||||||
Per plan, entity references use **file-local IDs** in saved scenes:
|
|
||||||
- Avoids brittle global IDs
|
|
||||||
- Enables scene prefabs/templates in future
|
|
||||||
- Automatically remapped on load
|
|
||||||
|
|
||||||
### Cross-Scene Reference Prevention
|
|
||||||
- Validation layer enforces no cross-references
|
|
||||||
- SerializeComponent throws exception if detected during save
|
|
||||||
- Plan notes: Use queries/singletons for cross-scene access
|
|
||||||
|
|
||||||
### JSON for Editor, Binary for Runtime
|
|
||||||
- Current impl: JSON serialization for editor (SceneSerializer.cs)
|
|
||||||
- Plan notes MemoryPack for runtime binary format
|
|
||||||
- Reflection allowed in editor, AOT-compatible runtime preserved
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
### Existing Components Used
|
|
||||||
- `SceneID` (Ghost.Engine/Components/SceneID.cs) - scene membership tag
|
|
||||||
- `Hierarchy` (Ghost.Engine/Components/Hierarchy.cs) - parent-child structure
|
|
||||||
- `LocalToWorld` - transform (referenced but not modified)
|
|
||||||
|
|
||||||
### ECS Integration
|
|
||||||
- Uses `QueryBuilder` and chunk iteration for queries
|
|
||||||
- `EntityManager` for all entity operations
|
|
||||||
- Archetype layouts for component enumeration
|
|
||||||
|
|
||||||
## What's NOT Implemented (As Per Plan)
|
|
||||||
|
|
||||||
The plan explicitly states:
|
|
||||||
> "Leave the actual UI implementation (TreeView) for later"
|
|
||||||
|
|
||||||
Not included in this implementation:
|
|
||||||
- WinUI 3 TreeView XAML
|
|
||||||
- UI controls for hierarchy panel
|
|
||||||
- Drag-drop entity reparenting
|
|
||||||
- Context menus
|
|
||||||
- Icons/gizmos
|
|
||||||
|
|
||||||
These are left for UI layer implementation. The data structures (SceneNode, EntityNode with ObservableCollection) are designed to bind directly to TreeView.
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
**Ghost.Engine:**
|
|
||||||
- `Scene.cs` - Scene struct + SceneManager
|
|
||||||
- `Systems/HierarchyUtility.cs` - Hierarchy helpers
|
|
||||||
|
|
||||||
**Ghost.Editor.Core:**
|
|
||||||
- `SceneGraph/SceneNode.cs` - Scene graph node
|
|
||||||
- `SceneGraph/EntityNode.cs` - Entity graph node
|
|
||||||
- `SceneGraph/EditorWorldManager.cs` - Central manager
|
|
||||||
- `Serialization/SceneData.cs` - JSON data structures
|
|
||||||
- `Serialization/SceneSerializer.cs` - Save/load logic
|
|
||||||
- `Validation/SceneValidator.cs` - Integrity checks
|
|
||||||
|
|
||||||
Total: 7 new files, ~1400 lines of code
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
To complete the scene graph feature:
|
|
||||||
1. **Create WinUI 3 TreeView** bound to `EditorWorldManager.LoadedScenes`
|
|
||||||
2. **Add toolbar**: New Scene, Save Scene, Unload Scene buttons
|
|
||||||
3. **Entity creation UI**: Right-click menu, "Create Empty" button
|
|
||||||
4. **Drag-drop**: Reparent entities by dragging in TreeView
|
|
||||||
5. **Rename**: Double-click to rename EntityNode.Name
|
|
||||||
6. **Integration**: Wire EditorWorldManager into main editor app lifecycle
|
|
||||||
|
|
||||||
All the core logic is complete and follows the plan precisely.
|
|
||||||
@@ -4,26 +4,31 @@ The Scene Graph is a hierarchical structure that represents all the objects and
|
|||||||
|
|
||||||
## Scene Graph (Editor representation of runtime data)
|
## Scene Graph (Editor representation of runtime data)
|
||||||
|
|
||||||
There should be two main types of nodes in the Scene Graph:
|
There should be three main types of nodes in the Scene Graph for now:
|
||||||
1. **Entity Node**: Represents an individual entity within a scene. Name stored here, not runtime component.
|
|
||||||
2. **Scene Node**: Represents a Scene object, which can contain multiple entities. Name stored here not runtime data.
|
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 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.
|
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
|
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.
|
1. Unload the runtime only systems like physics, rendering, 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.
|
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
|
### Editor Hierarchy
|
||||||
|
|
||||||
The Scene Graph should be represented as a tree structure in the editor (TreeView in WinUI 3), where:
|
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.
|
- The top level nodes represents the loaded Scenes in the editor world.
|
||||||
- Each Entity node can have child Entity nodes representing parent-child relationships between entities.
|
- 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:
|
An example hierarchy could look like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
- Scene 1
|
- Scene 1
|
||||||
- Entity A
|
- Entity A
|
||||||
@@ -38,28 +43,32 @@ An example hierarchy could look like this:
|
|||||||
A Scene is a collection of entities with SceneID component from a world that are grouped together. There can be multiple scenes in a world.
|
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
|
### 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 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.
|
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,
|
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 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 does not allow cross-scene references for now because ideally it's not a good practice to have cross-scene references.
|
||||||
We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
|
> We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
|
||||||
|
|
||||||
### Load a Scene
|
### Load a Scene
|
||||||
|
|
||||||
When loading a scene, we need to reconstruct the entities and their relationships based on the saved data.
|
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.
|
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.
|
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,
|
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.
|
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
|
### Data format
|
||||||
The scene data should be stored in a structured format (e.g., JSON or binary) that includes:
|
|
||||||
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
|
|
||||||
- References between entities using file local IDs
|
|
||||||
|
|
||||||
> The name of the saved scene file should match the name of the scene node in the editor.
|
The scene data should be stored in a structured format (e.g., JSON or binary) that includes:
|
||||||
|
|
||||||
|
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
|
||||||
|
- References between entities using file local IDs
|
||||||
|
|
||||||
|
> The name of the saved scene file should match the name of the scene node in the editor.
|
||||||
|
|
||||||
JSON should only be used in the editor and JSON serialization/deserialization logic should also only exist in the editor codebase (Ghost.Editor.Core). Reflection is allowed here.
|
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.
|
Binary format should be used in the runtime for better performance. The runtime codebase (Ghost.Engine) must be aot compatible.
|
||||||
@@ -68,10 +77,11 @@ Currently we strict the IComponent to must be unmanaged and blittable types.
|
|||||||
However, we also support ManagedEntity and ManagedEntityRef with ScriptComponent to allow OOP like logic for common gameplay logic that DOD pattern is not suitable for.
|
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.
|
Serializing/deserializing with those components will be tricky. We can use MemoryPack for binary serialization/deserialization because it supports both unmanaged and managed types.
|
||||||
|
|
||||||
## What you need to implement
|
## What need to implement
|
||||||
- Scene type for the runtime representation
|
|
||||||
- Scene Graph data structures (SceneNode, EntityNode)
|
- [ ] Scene type for the runtime representation if needed
|
||||||
- Editor World management (loading/unloading scenes, managing entities)
|
- [ ] Scene Graph data structures (SceneNode, EntityNode)
|
||||||
- Scene saving/loading logic with file local ID remapping
|
- [ ] Editor World management (loading/unloading scenes, managing entities)
|
||||||
- Serialization/deserialization logic for scene data (JSON for editor, binary for runtime)
|
- [ ] Scene saving/loading logic with file local ID remapping
|
||||||
- Leave the actual UI implementation (TreeView) for later, focus on the data structures and logic first but make sure the data structures are compatible with TreeView binding in WinUI 3.
|
- [ ] 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
|
||||||
|
|||||||
18
Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs
Normal file
18
Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.SceneGraph;
|
||||||
|
|
||||||
|
public abstract partial class SceneGraphNode : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string Name
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<SceneGraphNode> Children
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
} = new();
|
||||||
|
}
|
||||||
@@ -1,154 +1,5 @@
|
|||||||
using Ghost.Engine.Core;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
namespace Ghost.Editor.Core.SceneGraph;
|
||||||
|
|
||||||
/// <summary>
|
public sealed partial class SceneNode : SceneGraphNode
|
||||||
/// Represents a scene node in the editor scene graph hierarchy.
|
|
||||||
/// Contains editor-only metadata like display name and root entities.
|
|
||||||
/// </summary>
|
|
||||||
public class SceneNode
|
|
||||||
{
|
{
|
||||||
private string _name;
|
|
||||||
private readonly ObservableCollection<EntityNode> _rootEntities;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the runtime scene this node represents.
|
|
||||||
/// </summary>
|
|
||||||
public Scene Scene { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the display name for this scene in the editor.
|
|
||||||
/// This is NOT stored in runtime data.
|
|
||||||
/// </summary>
|
|
||||||
public string Name
|
|
||||||
{
|
|
||||||
get => _name;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_name = value;
|
|
||||||
OnNameChanged?.Invoke(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the file path where this scene is saved.
|
|
||||||
/// </summary>
|
|
||||||
public string? FilePath { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether this scene is currently loaded in the editor.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsLoaded { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether this scene has unsaved changes.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsDirty { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the collection of root entity nodes in this scene.
|
|
||||||
/// </summary>
|
|
||||||
public ObservableCollection<EntityNode> RootEntities => _rootEntities;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when the name property changes.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<SceneNode>? OnNameChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when root entities collection changes.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<SceneNode>? OnRootEntitiesChanged;
|
|
||||||
|
|
||||||
public SceneNode(Scene scene, string name = "New Scene")
|
|
||||||
{
|
|
||||||
Scene = scene;
|
|
||||||
_name = name;
|
|
||||||
_rootEntities = [];
|
|
||||||
_rootEntities.CollectionChanged += (s, e) => OnRootEntitiesChanged?.Invoke(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a root entity node to this scene.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entityNode">The entity node to add.</param>
|
|
||||||
public void AddRootEntity(EntityNode entityNode)
|
|
||||||
{
|
|
||||||
// Remove from previous parent if any
|
|
||||||
if (entityNode.Parent != null)
|
|
||||||
{
|
|
||||||
entityNode.Parent.RemoveChild(entityNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
entityNode.Parent = null;
|
|
||||||
entityNode.OwnerScene = this;
|
|
||||||
_rootEntities.Add(entityNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a root entity node from this scene.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entityNode">The entity node to remove.</param>
|
|
||||||
/// <returns>True if the entity was removed, false otherwise.</returns>
|
|
||||||
public bool RemoveRootEntity(EntityNode entityNode)
|
|
||||||
{
|
|
||||||
if (_rootEntities.Remove(entityNode))
|
|
||||||
{
|
|
||||||
entityNode.OwnerScene = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all entity nodes in this scene (root and descendants) in depth-first order.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An enumerable of all entity nodes in the scene.</returns>
|
|
||||||
public IEnumerable<EntityNode> GetAllEntities()
|
|
||||||
{
|
|
||||||
foreach (var root in _rootEntities)
|
|
||||||
{
|
|
||||||
yield return root;
|
|
||||||
|
|
||||||
foreach (var descendant in root.GetAllDescendants())
|
|
||||||
{
|
|
||||||
yield return descendant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds an entity node by its entity reference.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity">The entity to search for.</param>
|
|
||||||
/// <returns>The entity node if found, null otherwise.</returns>
|
|
||||||
public EntityNode? FindNode(Entity entity)
|
|
||||||
{
|
|
||||||
foreach (var root in _rootEntities)
|
|
||||||
{
|
|
||||||
var found = root.FindNode(entity);
|
|
||||||
if (found != null)
|
|
||||||
{
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marks this scene as dirty (has unsaved changes).
|
|
||||||
/// </summary>
|
|
||||||
public void MarkDirty()
|
|
||||||
{
|
|
||||||
IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{Name} (Scene ID: {Scene.ID}, Entities: {_rootEntities.Count})";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Serialization;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the serialized data for a scene in JSON format.
|
|
||||||
/// This is editor-only and used for scene file persistence.
|
|
||||||
/// </summary>
|
|
||||||
public class SceneData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the scene.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = "New Scene";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of entities in this scene, ordered by file-local ID.
|
|
||||||
/// The index in this list IS the file-local ID.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("entities")]
|
|
||||||
public List<EntityData> Entities { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the serialized data for an entity.
|
|
||||||
/// </summary>
|
|
||||||
public class EntityData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The display name of this entity (editor-only).
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = "Entity";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The file-local ID of the parent entity, or -1 if root.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("parent")]
|
|
||||||
public int ParentLocalId { get; set; } = -1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dictionary of component data, keyed by component type name.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("components")]
|
|
||||||
public Dictionary<string, ComponentData> Components { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the serialized data for a component.
|
|
||||||
/// </summary>
|
|
||||||
public class ComponentData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The component type full name (for deserialization).
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("type")]
|
|
||||||
public string TypeName { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The serialized component fields.
|
|
||||||
/// Entity references are stored as file-local IDs (integers).
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("fields")]
|
|
||||||
public Dictionary<string, object?> Fields { get; set; } = [];
|
|
||||||
}
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
using Ghost.Editor.Core.SceneGraph;
|
|
||||||
using Ghost.Editor.Core.Serialization;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using Ghost.Engine.Components;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Ghost.Engine.Core;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Serialization;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides functionality to save and load scenes with proper entity reference remapping.
|
|
||||||
/// </summary>
|
|
||||||
public class SceneSerializer
|
|
||||||
{
|
|
||||||
private readonly World _world;
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions s_jsonOptions = new()
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
IncludeFields = true
|
|
||||||
};
|
|
||||||
|
|
||||||
public SceneSerializer(World world)
|
|
||||||
{
|
|
||||||
_world = world;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves a scene to a JSON file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneNode">The scene node to save.</param>
|
|
||||||
/// <param name="filePath">The path to save the scene to.</param>
|
|
||||||
public void SaveScene(SceneNode sceneNode, string filePath)
|
|
||||||
{
|
|
||||||
var sceneData = new SceneData
|
|
||||||
{
|
|
||||||
Name = sceneNode.Name
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build a mapping of Entity -> FileLocalID
|
|
||||||
var allEntities = sceneNode.GetAllEntities().ToList();
|
|
||||||
var entityToLocalId = new Dictionary<Entity, int>();
|
|
||||||
|
|
||||||
for (int i = 0; i < allEntities.Count; i++)
|
|
||||||
{
|
|
||||||
entityToLocalId[allEntities[i].Entity] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize each entity
|
|
||||||
foreach (var entityNode in allEntities)
|
|
||||||
{
|
|
||||||
var entityData = SerializeEntity(entityNode, entityToLocalId, sceneNode.Scene.ID);
|
|
||||||
sceneData.Entities.Add(entityData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to file
|
|
||||||
var json = JsonSerializer.Serialize(sceneData, s_jsonOptions);
|
|
||||||
File.WriteAllText(filePath, json);
|
|
||||||
|
|
||||||
sceneNode.FilePath = filePath;
|
|
||||||
sceneNode.IsDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads a scene from a JSON file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filePath">The path to load the scene from.</param>
|
|
||||||
/// <param name="sceneManager">The scene manager to create the scene in.</param>
|
|
||||||
/// <param name="worldManager">The editor world manager.</param>
|
|
||||||
/// <returns>The loaded scene node.</returns>
|
|
||||||
public SceneNode LoadScene(string filePath, SceneManager sceneManager, EditorWorldManager worldManager)
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(filePath);
|
|
||||||
var sceneData = JsonSerializer.Deserialize<SceneData>(json, s_jsonOptions);
|
|
||||||
|
|
||||||
if (sceneData == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Failed to deserialize scene from {filePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new scene
|
|
||||||
var sceneNode = worldManager.CreateNewScene(sceneData.Name);
|
|
||||||
sceneNode.FilePath = filePath;
|
|
||||||
|
|
||||||
// Build file-local ID -> Entity mapping
|
|
||||||
var localIdToEntity = new Dictionary<int, Entity>();
|
|
||||||
var localIdToEntityNode = new Dictionary<int, EntityNode>();
|
|
||||||
|
|
||||||
// First pass: Create all entities
|
|
||||||
for (int i = 0; i < sceneData.Entities.Count; i++)
|
|
||||||
{
|
|
||||||
var entityDataItem = sceneData.Entities[i];
|
|
||||||
|
|
||||||
// Create runtime entity
|
|
||||||
var entity = _world.EntityManager.CreateEntity();
|
|
||||||
_world.EntityManager.AddComponent(entity, new SceneID { id = sceneNode.Scene.ID });
|
|
||||||
|
|
||||||
// Create entity node
|
|
||||||
var entityNode = new EntityNode(entity, entityDataItem.Name);
|
|
||||||
|
|
||||||
localIdToEntity[i] = entity;
|
|
||||||
localIdToEntityNode[i] = entityNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: Deserialize components and setup hierarchy
|
|
||||||
for (int i = 0; i < sceneData.Entities.Count; i++)
|
|
||||||
{
|
|
||||||
var entityDataItem = sceneData.Entities[i];
|
|
||||||
var entity = localIdToEntity[i];
|
|
||||||
var entityNode = localIdToEntityNode[i];
|
|
||||||
|
|
||||||
// Deserialize each component
|
|
||||||
foreach (var (typeName, componentData) in entityDataItem.Components)
|
|
||||||
{
|
|
||||||
DeserializeComponent(entity, componentData, localIdToEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup hierarchy in scene graph
|
|
||||||
if (entityDataItem.ParentLocalId >= 0 && localIdToEntityNode.TryGetValue(entityDataItem.ParentLocalId, out var parentNode))
|
|
||||||
{
|
|
||||||
parentNode.AddChild(entityNode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sceneNode.AddRootEntity(entityNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sceneNode.IsDirty = false;
|
|
||||||
return sceneNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EntityData SerializeEntity(EntityNode entityNode, Dictionary<Entity, int> entityToLocalId, short sceneId)
|
|
||||||
{
|
|
||||||
var entityData = new EntityData
|
|
||||||
{
|
|
||||||
Name = entityNode.Name,
|
|
||||||
ParentLocalId = entityNode.Parent != null && entityToLocalId.TryGetValue(entityNode.Parent.Entity, out var parentId)
|
|
||||||
? parentId
|
|
||||||
: -1
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get entity location
|
|
||||||
var location = _world.EntityManager.GetEntityLocation(entityNode.Entity);
|
|
||||||
if (!location.IsSuccess)
|
|
||||||
{
|
|
||||||
return entityData;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(location.Value.archetypeID);
|
|
||||||
|
|
||||||
// Iterate through all components in the archetype
|
|
||||||
for (int i = 0; i < archetype._layouts.Count; i++)
|
|
||||||
{
|
|
||||||
var layout = archetype._layouts[i];
|
|
||||||
if (layout.enableBitsOffset == -1) continue; // Skip invalid layouts
|
|
||||||
|
|
||||||
var componentTypeId = layout.componentID;
|
|
||||||
|
|
||||||
// Skip SceneID component - it's implicit
|
|
||||||
if (componentTypeId == ComponentTypeID<SceneID>.Value)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get component type
|
|
||||||
if (!ComponentRegistry.s_runtimeIDToType.TryGetValue(componentTypeId, out var componentType))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the component
|
|
||||||
var componentData = SerializeComponent(
|
|
||||||
entityNode.Entity,
|
|
||||||
location.Value,
|
|
||||||
componentType,
|
|
||||||
componentTypeId,
|
|
||||||
entityToLocalId,
|
|
||||||
sceneId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (componentData != null)
|
|
||||||
{
|
|
||||||
entityData.Components[componentType.FullName ?? componentType.Name] = componentData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entityData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe ComponentData? SerializeComponent(
|
|
||||||
Entity entity,
|
|
||||||
EntityLocation location,
|
|
||||||
Type componentType,
|
|
||||||
int componentTypeId,
|
|
||||||
Dictionary<Entity, int> entityToLocalId,
|
|
||||||
short sceneId)
|
|
||||||
{
|
|
||||||
// Get component data pointer
|
|
||||||
var pComponent = _world.EntityManager.GetComponent(entity, componentTypeId);
|
|
||||||
if (pComponent == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var componentData = new ComponentData
|
|
||||||
{
|
|
||||||
TypeName = componentType.FullName ?? componentType.Name
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serialize each field
|
|
||||||
var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
|
|
||||||
foreach (var field in fields)
|
|
||||||
{
|
|
||||||
var fieldValue = field.GetValue(Marshal.PtrToStructure((IntPtr)pComponent, componentType));
|
|
||||||
|
|
||||||
// Check if this field is an Entity reference
|
|
||||||
if (field.FieldType == typeof(Entity))
|
|
||||||
{
|
|
||||||
var entityRef = (Entity)fieldValue!;
|
|
||||||
|
|
||||||
if (entityRef.IsValid)
|
|
||||||
{
|
|
||||||
// Validate: Entity must be in the same scene
|
|
||||||
if (_world.EntityManager.HasComponent<SceneID>(entityRef))
|
|
||||||
{
|
|
||||||
var refSceneId = _world.EntityManager.GetComponent<SceneID>(entityRef);
|
|
||||||
if (refSceneId.id != sceneId)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Cross-scene reference detected! Entity {entity} references entity {entityRef} from different scene. This is not allowed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to file-local ID
|
|
||||||
if (entityToLocalId.TryGetValue(entityRef, out var localId))
|
|
||||||
{
|
|
||||||
componentData.Fields[field.Name] = localId;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Entity not found in the scene - this shouldn't happen after validation
|
|
||||||
componentData.Fields[field.Name] = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
componentData.Fields[field.Name] = -1; // Invalid entity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Store as-is for other types
|
|
||||||
componentData.Fields[field.Name] = fieldValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return componentData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void DeserializeComponent(Entity entity, ComponentData componentData, Dictionary<int, Entity> localIdToEntity)
|
|
||||||
{
|
|
||||||
// Get component type
|
|
||||||
var componentType = Type.GetType(componentData.TypeName);
|
|
||||||
if (componentType == null)
|
|
||||||
{
|
|
||||||
// Try to find in loaded assemblies
|
|
||||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
|
||||||
{
|
|
||||||
componentType = assembly.GetType(componentData.TypeName);
|
|
||||||
if (componentType != null) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentType == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Warning: Component type {componentData.TypeName} not found. Skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get component ID
|
|
||||||
var componentTypeId = ComponentRegistry.GetComponentID(componentType);
|
|
||||||
if (componentTypeId.IsInvalid)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Warning: Component {componentData.TypeName} not registered. Skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create instance
|
|
||||||
var componentInstance = Activator.CreateInstance(componentType);
|
|
||||||
if (componentInstance == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deserialize fields
|
|
||||||
var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
|
|
||||||
foreach (var field in fields)
|
|
||||||
{
|
|
||||||
if (!componentData.Fields.TryGetValue(field.Name, out var fieldValue))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Entity references
|
|
||||||
if (field.FieldType == typeof(Entity))
|
|
||||||
{
|
|
||||||
if (fieldValue is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Number)
|
|
||||||
{
|
|
||||||
var localId = jsonElement.GetInt32();
|
|
||||||
|
|
||||||
if (localId >= 0 && localIdToEntity.TryGetValue(localId, out var targetEntity))
|
|
||||||
{
|
|
||||||
field.SetValue(componentInstance, targetEntity);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
field.SetValue(componentInstance, Entity.Invalid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Handle other types - may need type conversion from JsonElement
|
|
||||||
if (fieldValue is JsonElement jsonElem)
|
|
||||||
{
|
|
||||||
var converted = JsonSerializer.Deserialize(jsonElem.GetRawText(), field.FieldType, s_jsonOptions);
|
|
||||||
field.SetValue(componentInstance, converted);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
field.SetValue(componentInstance, fieldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add component to entity
|
|
||||||
var componentPtr = Marshal.AllocHGlobal(Marshal.SizeOf(componentType));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Marshal.StructureToPtr(componentInstance, componentPtr, false);
|
|
||||||
_world.EntityManager.AddComponent(entity, componentTypeId, (void*)componentPtr);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Marshal.FreeHGlobal(componentPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
namespace Ghost.Editor.Core.Resources;
|
namespace Ghost.Editor.Core.Utilities;
|
||||||
|
|
||||||
internal static class FileExtensions
|
internal static class FileExtensions
|
||||||
{
|
{
|
||||||
|
public const string META_FILE_EXTENSION = ".gmeta";
|
||||||
|
|
||||||
public const string PROJECT_FILE_EXTENSION = ".gproj";
|
public const string PROJECT_FILE_EXTENSION = ".gproj";
|
||||||
public const string TEMPLATE_FILE_EXTENSION = ".gtmpl";
|
public const string TEMPLATE_FILE_EXTENSION = ".gtmpl";
|
||||||
public const string SCENE_FILE_EXTENSION = ".gscene";
|
public const string SCENE_FILE_EXTENSION = ".gscene";
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
using Ghost.Entities;
|
|
||||||
using Ghost.Engine.Components;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Validation;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validation result for scene integrity checks.
|
|
||||||
/// </summary>
|
|
||||||
public class ValidationResult
|
|
||||||
{
|
|
||||||
public bool IsValid => Errors.Count == 0;
|
|
||||||
public List<string> Errors { get; } = [];
|
|
||||||
public List<string> Warnings { get; } = [];
|
|
||||||
|
|
||||||
public void AddError(string error) => Errors.Add(error);
|
|
||||||
public void AddWarning(string warning) => Warnings.Add(warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides validation for scenes, checking for cross-scene references and other integrity issues.
|
|
||||||
/// </summary>
|
|
||||||
public class SceneValidator
|
|
||||||
{
|
|
||||||
private readonly World _world;
|
|
||||||
|
|
||||||
public SceneValidator(World world)
|
|
||||||
{
|
|
||||||
_world = world;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that all entity references within a scene point to entities in the same scene.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneId">The ID of the scene to validate.</param>
|
|
||||||
/// <param name="entities">The list of entities in the scene.</param>
|
|
||||||
/// <returns>A validation result with any errors or warnings found.</returns>
|
|
||||||
public ValidationResult ValidateSceneReferences(short sceneId, IEnumerable<Entity> entities)
|
|
||||||
{
|
|
||||||
var result = new ValidationResult();
|
|
||||||
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
// Get entity location
|
|
||||||
var location = _world.EntityManager.GetEntityLocation(entity);
|
|
||||||
if (!location.IsSuccess)
|
|
||||||
{
|
|
||||||
result.AddError($"Entity {entity} not found in world.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(location.Value.archetypeID);
|
|
||||||
|
|
||||||
// Check each component for entity references
|
|
||||||
for (int i = 0; i < archetype._layouts.Count; i++)
|
|
||||||
{
|
|
||||||
var layout = archetype._layouts[i];
|
|
||||||
if (layout.enableBitsOffset == -1) continue;
|
|
||||||
|
|
||||||
var componentTypeId = layout.componentID;
|
|
||||||
|
|
||||||
// Get component type
|
|
||||||
if (!ComponentRegistry.s_runtimeIDToType.TryGetValue(componentTypeId.Value, out var componentType))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get component data
|
|
||||||
var pComponent = _world.EntityManager.GetComponent(entity, componentTypeId);
|
|
||||||
if (pComponent == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check fields for entity references
|
|
||||||
ValidateComponentReferences(entity, componentType, pComponent, sceneId, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that a scene has no circular hierarchy references.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entities">The list of entities in the scene.</param>
|
|
||||||
/// <returns>A validation result with any errors or warnings found.</returns>
|
|
||||||
public ValidationResult ValidateHierarchy(IEnumerable<Entity> entities)
|
|
||||||
{
|
|
||||||
var result = new ValidationResult();
|
|
||||||
var entitySet = new HashSet<Entity>(entities);
|
|
||||||
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
if (!_world.EntityManager.HasComponent<Hierarchy>(entity))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var visited = new HashSet<Entity>();
|
|
||||||
var current = entity;
|
|
||||||
|
|
||||||
// Traverse up the hierarchy
|
|
||||||
while (current.IsValid)
|
|
||||||
{
|
|
||||||
if (visited.Contains(current))
|
|
||||||
{
|
|
||||||
result.AddError($"Circular hierarchy detected involving entity {entity}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.Add(current);
|
|
||||||
|
|
||||||
ref var hierarchy = ref _world.EntityManager.GetComponent<Hierarchy>(current);
|
|
||||||
current = hierarchy.parent;
|
|
||||||
|
|
||||||
// Check that parent is in the same scene if it exists
|
|
||||||
if (current.IsValid && !entitySet.Contains(current))
|
|
||||||
{
|
|
||||||
result.AddWarning($"Entity {entity} has parent {current} outside of the scene.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void ValidateComponentReferences(
|
|
||||||
Entity entity,
|
|
||||||
Type componentType,
|
|
||||||
void* pComponent,
|
|
||||||
short sceneId,
|
|
||||||
ValidationResult result)
|
|
||||||
{
|
|
||||||
var componentInstance = Marshal.PtrToStructure((IntPtr)pComponent, componentType);
|
|
||||||
if (componentInstance == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
|
|
||||||
foreach (var field in fields)
|
|
||||||
{
|
|
||||||
if (field.FieldType == typeof(Entity))
|
|
||||||
{
|
|
||||||
var entityRef = (Entity)field.GetValue(componentInstance)!;
|
|
||||||
|
|
||||||
if (!entityRef.IsValid)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the referenced entity exists
|
|
||||||
if (!_world.EntityManager.Exists(entityRef))
|
|
||||||
{
|
|
||||||
result.AddError($"Entity {entity} in component {componentType.Name} references invalid entity {entityRef} in field {field.Name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the referenced entity has a SceneID
|
|
||||||
if (!_world.EntityManager.HasComponent<SceneID>(entityRef))
|
|
||||||
{
|
|
||||||
result.AddWarning($"Entity {entity} in component {componentType.Name} references entity {entityRef} without a SceneID in field {field.Name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the referenced entity is in the same scene
|
|
||||||
var refSceneId = _world.EntityManager.GetComponent<SceneID>(entityRef);
|
|
||||||
if (refSceneId.id != sceneId)
|
|
||||||
{
|
|
||||||
result.AddError($"Cross-scene reference detected! Entity {entity} in scene {sceneId} references entity {entityRef} in scene {refSceneId.id} via component {componentType.Name}.{field.Name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
using Ghost.Entities;
|
|
||||||
using Ghost.Engine.Components;
|
|
||||||
|
|
||||||
namespace Ghost.Engine.Systems;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides utility methods for working with entity hierarchies.
|
|
||||||
/// </summary>
|
|
||||||
public static class HierarchyUtility
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the parent of an entity, updating the Hierarchy component accordingly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world containing the entities.</param>
|
|
||||||
/// <param name="child">The child entity.</param>
|
|
||||||
/// <param name="parent">The parent entity, or Entity.Invalid to make the entity a root.</param>
|
|
||||||
public static void SetParent(World world, Entity child, Entity parent)
|
|
||||||
{
|
|
||||||
if (!world.EntityManager.HasComponent<Hierarchy>(child))
|
|
||||||
{
|
|
||||||
world.EntityManager.AddComponent(child, Hierarchy.Root);
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var childHierarchy = ref world.EntityManager.GetComponent<Hierarchy>(child);
|
|
||||||
|
|
||||||
// Remove from old parent's children list
|
|
||||||
if (childHierarchy.parent.IsValid)
|
|
||||||
{
|
|
||||||
RemoveFromSiblingList(world, child, childHierarchy.parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new parent
|
|
||||||
childHierarchy.parent = parent;
|
|
||||||
|
|
||||||
if (parent.IsValid)
|
|
||||||
{
|
|
||||||
if (!world.EntityManager.HasComponent<Hierarchy>(parent))
|
|
||||||
{
|
|
||||||
world.EntityManager.AddComponent(parent, Hierarchy.Root);
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var parentHierarchy = ref world.EntityManager.GetComponent<Hierarchy>(parent);
|
|
||||||
|
|
||||||
// Add to parent's children list
|
|
||||||
childHierarchy.nextSibling = parentHierarchy.firstChild;
|
|
||||||
parentHierarchy.firstChild = child;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
childHierarchy.nextSibling = Entity.Invalid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the parent of an entity.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world containing the entity.</param>
|
|
||||||
/// <param name="entity">The entity to get the parent of.</param>
|
|
||||||
/// <returns>The parent entity, or Entity.Invalid if the entity has no parent.</returns>
|
|
||||||
public static Entity GetParent(World world, Entity entity)
|
|
||||||
{
|
|
||||||
if (!world.EntityManager.HasComponent<Hierarchy>(entity))
|
|
||||||
{
|
|
||||||
return Entity.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var hierarchy = ref world.EntityManager.GetComponent<Hierarchy>(entity);
|
|
||||||
return hierarchy.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all children of an entity.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world containing the entity.</param>
|
|
||||||
/// <param name="parent">The parent entity.</param>
|
|
||||||
/// <param name="children">Span to store the children.</param>
|
|
||||||
/// <returns>The number of children written to the span.</returns>
|
|
||||||
public static int GetChildren(World world, Entity parent, Span<Entity> children)
|
|
||||||
{
|
|
||||||
if (!world.EntityManager.HasComponent<Hierarchy>(parent))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var hierarchy = ref world.EntityManager.GetComponent<Hierarchy>(parent);
|
|
||||||
var currentChild = hierarchy.firstChild;
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
while (currentChild.IsValid && count < children.Length)
|
|
||||||
{
|
|
||||||
children[count++] = currentChild;
|
|
||||||
|
|
||||||
if (world.EntityManager.HasComponent<Hierarchy>(currentChild))
|
|
||||||
{
|
|
||||||
ref var childHierarchy = ref world.EntityManager.GetComponent<Hierarchy>(currentChild);
|
|
||||||
currentChild = childHierarchy.nextSibling;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all descendants of an entity (children, grandchildren, etc.) in depth-first order.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world containing the entity.</param>
|
|
||||||
/// <param name="root">The root entity.</param>
|
|
||||||
/// <param name="descendants">List to store the descendants.</param>
|
|
||||||
public static void GetDescendants(World world, Entity root, List<Entity> descendants)
|
|
||||||
{
|
|
||||||
Span<Entity> children = stackalloc Entity[32];
|
|
||||||
var childCount = GetChildren(world, root, children);
|
|
||||||
|
|
||||||
for (int i = 0; i < childCount; i++)
|
|
||||||
{
|
|
||||||
var child = children[i];
|
|
||||||
descendants.Add(child);
|
|
||||||
GetDescendants(world, child, descendants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a child from its parent's sibling list.
|
|
||||||
/// </summary>
|
|
||||||
private static void RemoveFromSiblingList(World world, Entity child, Entity parent)
|
|
||||||
{
|
|
||||||
if (!world.EntityManager.HasComponent<Hierarchy>(parent))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var parentHierarchy = ref world.EntityManager.GetComponent<Hierarchy>(parent);
|
|
||||||
ref var childHierarchy = ref world.EntityManager.GetComponent<Hierarchy>(child);
|
|
||||||
|
|
||||||
// If child is the first child
|
|
||||||
if (parentHierarchy.firstChild.Equals(child))
|
|
||||||
{
|
|
||||||
parentHierarchy.firstChild = childHierarchy.nextSibling;
|
|
||||||
childHierarchy.nextSibling = Entity.Invalid;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the previous sibling
|
|
||||||
var currentSibling = parentHierarchy.firstChild;
|
|
||||||
|
|
||||||
while (currentSibling.IsValid)
|
|
||||||
{
|
|
||||||
if (!world.EntityManager.HasComponent<Hierarchy>(currentSibling))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var siblingHierarchy = ref world.EntityManager.GetComponent<Hierarchy>(currentSibling);
|
|
||||||
|
|
||||||
if (siblingHierarchy.nextSibling.Equals(child))
|
|
||||||
{
|
|
||||||
siblingHierarchy.nextSibling = childHierarchy.nextSibling;
|
|
||||||
childHierarchy.nextSibling = Entity.Invalid;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSibling = siblingHierarchy.nextSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if an entity is an ancestor of another entity.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="world">The world containing the entities.</param>
|
|
||||||
/// <param name="potentialAncestor">The potential ancestor entity.</param>
|
|
||||||
/// <param name="descendant">The descendant entity.</param>
|
|
||||||
/// <returns>True if potentialAncestor is an ancestor of descendant, false otherwise.</returns>
|
|
||||||
public static bool IsAncestor(World world, Entity potentialAncestor, Entity descendant)
|
|
||||||
{
|
|
||||||
var current = GetParent(world, descendant);
|
|
||||||
|
|
||||||
while (current.IsValid)
|
|
||||||
{
|
|
||||||
if (current.Equals(potentialAncestor))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = GetParent(world, current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user