forked from Misaki/GhostEngine
Remove old SceneGraph
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user