diff --git a/source/Vignette.Desktop/Program.cs b/source/Vignette.Desktop/Program.cs index df58c82..ea628d9 100644 --- a/source/Vignette.Desktop/Program.cs +++ b/source/Vignette.Desktop/Program.cs @@ -2,6 +2,21 @@ // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. using Sekai; +using Sekai.GLFW; +using Sekai.OpenAL; +using Sekai.OpenGL; using Vignette; -Host.Run(new HostOptions { Name = "Vignette" }); +var options = new VignetteGameOptions(); +options.UseOpenAL(); +options.UseOpenGL(); +options.UseScripts(); + +if (RuntimeInfo.IsDesktop) +{ + options.UseGLFW(); + options.Window.Title = "Vignette"; +} + +var game = new VignetteGame(options); +game.Run(); diff --git a/source/Vignette.Desktop/Vignette.Desktop.csproj b/source/Vignette.Desktop/Vignette.Desktop.csproj index fc1497c..75cb543 100644 --- a/source/Vignette.Desktop/Vignette.Desktop.csproj +++ b/source/Vignette.Desktop/Vignette.Desktop.csproj @@ -10,9 +10,9 @@ - - - + + + diff --git a/source/Vignette/Audio/AudioManager.cs b/source/Vignette/Audio/AudioManager.cs index e461756..22dd825 100644 --- a/source/Vignette/Audio/AudioManager.cs +++ b/source/Vignette/Audio/AudioManager.cs @@ -29,6 +29,11 @@ internal AudioManager(AudioDevice device) /// An audio controller. public IAudioController GetController(AudioStream stream) { + if (device is null) + { + throw new InvalidOperationException("The audio manager has not yet been initialized."); + } + return new StreamingAudioController(device.CreateSource(), stream, this); } @@ -61,6 +66,11 @@ public void Return(IAudioController controller) AudioBuffer IObjectPool.Get() { + if (device is null) + { + throw new InvalidOperationException("The audio manager has not yet been initialized."); + } + if (!bufferPool.TryTake(out var buffer)) { buffer = device.CreateBuffer(); @@ -102,13 +112,13 @@ public TimeSpan Position private const int max_buffer_stream = 4; private readonly AudioSource source; private readonly AudioStream stream; - private readonly IObjectPool bufferPool; + private readonly IObjectPool buffer; - public StreamingAudioController(AudioSource source, AudioStream stream, IObjectPool bufferPool) + public StreamingAudioController(AudioSource source, AudioStream stream, IObjectPool buffer) { this.source = source; this.stream = stream; - this.bufferPool = bufferPool; + this.buffer = buffer; } public void Play() @@ -119,14 +129,15 @@ public void Play() for (int i = 0; i < max_buffer_stream; i++) { - var buffer = bufferPool.Get(); + var b = buffer.Get(); - if (!allocate(buffer)) + if (!allocate(b)) { + buffer.Return(b); break; } - source.Enqueue(buffer); + source.Enqueue(b); } } @@ -168,7 +179,7 @@ public void Dispose() while(source.TryDequeue(out var buffer)) { - bufferPool.Return(buffer); + this.buffer.Return(buffer); } source.Dispose(); diff --git a/source/Vignette/Behavior.cs b/source/Vignette/Behavior.cs index c13d7a2..eb4e32f 100644 --- a/source/Vignette/Behavior.cs +++ b/source/Vignette/Behavior.cs @@ -2,17 +2,20 @@ // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. using System; +using Jint.Native; +using Vignette.Scripting; namespace Vignette; /// -/// A that processes itself per-frame. +/// A that has behavior. /// -public abstract class Behavior : Node, IComparable +public class Behavior : Node, IComparable { /// - /// The processing order for this . + /// The processing order for this . /// + [ScriptVisible] public int Order { get => order; @@ -29,8 +32,9 @@ public int Order } /// - /// Whether this should be enabled or not affecting calls. + /// Whether this should perform calls. /// + [ScriptVisible] public bool Enabled { get => enabled; @@ -57,31 +61,34 @@ public bool Enabled public event EventHandler? EnabledChanged; private int order; - private bool enabled = true; + private bool enabled = false; /// - /// Called once in the update loop after the has entered the node graph. + /// Called once after entering a world. /// public virtual void Load() { + Invoke(load); } /// - /// Called every frame to perform updates on this . + /// Called every frame. /// - /// The time elapsed between frames. + /// The elapsed time between frames. public virtual void Update(TimeSpan elapsed) { + Invoke(update, elapsed.TotalSeconds); } /// - /// Called once in the update loop before the exits the node graph. + /// Called once before leaving a world. /// public virtual void Unload() { + Invoke(unload); } - public int CompareTo(Behavior? other) + int IComparable.CompareTo(Behavior? other) { if (other is null) { @@ -97,4 +104,8 @@ public int CompareTo(Behavior? other) return Order.CompareTo(other.Order); } + + private static readonly JsValue load = new JsString("load"); + private static readonly JsValue update = new JsString("update"); + private static readonly JsValue unload = new JsString("unload"); } diff --git a/source/Vignette/Content/ContentManager.cs b/source/Vignette/Content/ContentManager.cs index cb3f855..c1f1ba5 100644 --- a/source/Vignette/Content/ContentManager.cs +++ b/source/Vignette/Content/ContentManager.cs @@ -113,14 +113,10 @@ public T Load(ReadOnlySpan bytes) continue; } - try + if (typedLoader.TryLoad(bytes, out result)) { - result = typedLoader.Load(bytes); break; } - catch - { - } } if (result is null) @@ -131,20 +127,15 @@ public T Load(ReadOnlySpan bytes) return result; } - /// - /// Adds a content loader to the content manager. - /// - /// The content loader to add. - /// The file extensions supported by this loader. - /// Thrown when - internal void Add(IContentLoader loader, params string[] extensions) + private void add(IContentLoader loader, params string[] extensions) { foreach (string extension in extensions) { string ext = extension.StartsWith(ext_separator) ? extension : ext_separator + extension; - this.loaders.Add(loader); this.extensions.Add(ext); } + + loaders.Add(loader); } private const char ext_separator = '.'; diff --git a/source/Vignette/Content/IContentLoader.cs b/source/Vignette/Content/IContentLoader.cs index 9a73a15..6264c6d 100644 --- a/source/Vignette/Content/IContentLoader.cs +++ b/source/Vignette/Content/IContentLoader.cs @@ -2,6 +2,7 @@ // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. using System; +using System.Diagnostics.CodeAnalysis; namespace Vignette.Content; @@ -25,4 +26,24 @@ public interface IContentLoader : IContentLoader /// The byte data to be read. /// The loaded content. T Load(ReadOnlySpan bytes); + + /// + /// Loads a as . + /// + /// The byte data to be read. + /// The loaded content. + /// if the content has been loaded. Otherwise, . + bool TryLoad(ReadOnlySpan bytes, [NotNullWhen(true)] out T? result) + { + try + { + result = Load(bytes); + return true; + } + catch + { + result = null; + return false; + } + } } diff --git a/source/Vignette/Drawable.cs b/source/Vignette/Drawable.cs index e24023b..104b969 100644 --- a/source/Vignette/Drawable.cs +++ b/source/Vignette/Drawable.cs @@ -2,18 +2,22 @@ // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. using System; +using System.Numerics; +using Jint.Native; using Vignette.Graphics; +using Vignette.Scripting; namespace Vignette; /// -/// A that is capable of drawing. +/// A that can be drawn. /// -public abstract class Drawable : Behavior +public class Drawable : Behavior, ISpatialObject { /// - /// Whether this should be drawn or not. + /// Whether this should perform calls. /// + [ScriptVisible] public bool Visible { get => visible; @@ -29,18 +33,48 @@ public bool Visible } } + /// + /// The node's scaling. + /// + [ScriptVisible] + public Vector3 Scale { get; set; } = Vector3.One; + + /// + /// The node's shearing. + /// + [ScriptVisible] + public Vector3 Shear + { + get => new(shear[0, 1], shear[0, 2], shear[1, 2]); + set + { + shear[0, 1] = value.X; + shear[0, 2] = value.Y; + shear[1, 2] = value.Z; + } + } + /// /// Called when has been changed. /// public event EventHandler? VisibleChanged; - private bool visible = true; + private bool visible = false; + private Matrix4x4 shear = Matrix4x4.Identity; /// - /// Called every frame to perform drawing operations on this . + /// The node's matrix. /// - /// The drawable rendering context. + protected override Matrix4x4 Matrix => shear * Matrix4x4.CreateScale(Scale) * base.Matrix; + + /// + /// Called when performing draw operations. + /// + /// The rendering context. public virtual void Draw(RenderContext context) { + Invoke(draw, context); } + + private static readonly JsValue draw = new JsString("draw"); } diff --git a/source/Vignette/Graphics/IPointObject.cs b/source/Vignette/Graphics/IPointObject.cs new file mode 100644 index 0000000..34cc19e --- /dev/null +++ b/source/Vignette/Graphics/IPointObject.cs @@ -0,0 +1,27 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System.Numerics; + +namespace Vignette.Graphics; + +/// +/// An object that has a position and rotation +/// +public interface IPointObject +{ + /// + /// The point's position. + /// + Vector3 Position { get; } + + /// + /// The point's rotation. + /// + Vector3 Rotation { get; } + + /// + /// The point's matrix. + /// + Matrix4x4 Matrix { get; } +} diff --git a/source/Vignette/Graphics/IProjector.cs b/source/Vignette/Graphics/IProjector.cs index 17601b1..69abf60 100644 --- a/source/Vignette/Graphics/IProjector.cs +++ b/source/Vignette/Graphics/IProjector.cs @@ -9,18 +9,8 @@ namespace Vignette.Graphics; /// /// An interface for objects providing clip space info. /// -public interface IProjector +public interface IProjector : IPointObject { - /// - /// The projector's position. - /// - Vector3 Position { get; } - - /// - /// The projector's rotation. - /// - Vector3 Rotation { get; } - /// /// The projector's view matrix. /// diff --git a/source/Vignette/Graphics/ISpatialObject.cs b/source/Vignette/Graphics/ISpatialObject.cs new file mode 100644 index 0000000..2cb6824 --- /dev/null +++ b/source/Vignette/Graphics/ISpatialObject.cs @@ -0,0 +1,22 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System.Numerics; + +namespace Vignette.Graphics; + +/// +/// An interface for objects providing spatial info. +/// +public interface ISpatialObject : IPointObject +{ + /// + /// The world's scaling. + /// + Vector3 Scale { get; } + + /// + /// The world's shearing. + /// + Vector3 Shear { get; } +} diff --git a/source/Vignette/Graphics/IWorld.cs b/source/Vignette/Graphics/IWorld.cs deleted file mode 100644 index b06f28e..0000000 --- a/source/Vignette/Graphics/IWorld.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Cosyne -// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. - -using System.Numerics; - -namespace Vignette.Graphics; - -/// -/// An interface for objects providing world space info. -/// -public interface IWorld -{ - /// - /// The world's position. - /// - Vector3 Position { get; } - - /// - /// The world's rotation. - /// - Vector3 Rotation { get; } - - /// - /// The world's scaling. - /// - Vector3 Scale { get; } - - /// - /// The world's shearing. - /// - Vector3 Shear { get; } - - /// - /// The world's local matrix. - /// - Matrix4x4 LocalMatrix { get; } - - /// - /// The world's world matrix. - /// - Matrix4x4 WorldMatrix { get; } -} diff --git a/source/Vignette/Graphics/RenderContext.cs b/source/Vignette/Graphics/RenderContext.cs index eff0d59..e18730e 100644 --- a/source/Vignette/Graphics/RenderContext.cs +++ b/source/Vignette/Graphics/RenderContext.cs @@ -1,6 +1,8 @@ // Copyright (c) Cosyne // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. +using Vignette.Scripting; + namespace Vignette.Graphics; /// @@ -8,11 +10,11 @@ namespace Vignette.Graphics; /// public readonly struct RenderContext { - private readonly IWorld world; + private readonly ISpatialObject world; private readonly RenderQueue queue; private readonly IProjector projector; - internal RenderContext(RenderQueue queue, IProjector projector, IWorld world) + internal RenderContext(RenderQueue queue, IProjector projector, ISpatialObject world) { this.queue = queue; this.world = world; @@ -23,6 +25,7 @@ internal RenderContext(RenderQueue queue, IProjector projector, IWorld world) /// Draws a render object. /// /// The to draw. + [ScriptVisible] public void Draw(RenderObject renderObject) { queue.Enqueue(projector, world, renderObject); diff --git a/source/Vignette/Graphics/RenderData.cs b/source/Vignette/Graphics/RenderData.cs index 910f7fa..e1f6efa 100644 --- a/source/Vignette/Graphics/RenderData.cs +++ b/source/Vignette/Graphics/RenderData.cs @@ -11,7 +11,7 @@ public readonly struct RenderData /// /// The world. /// - public IWorld World { get; } + public ISpatialObject Spatial { get; } /// /// The projector. @@ -23,9 +23,9 @@ public readonly struct RenderData /// public RenderObject Renderable { get; } - public RenderData(IProjector projector, IWorld world, RenderObject renderable) + public RenderData(IProjector projector, ISpatialObject spatial, RenderObject renderable) { - World = world; + Spatial = spatial; Projector = projector; Renderable = renderable; } diff --git a/source/Vignette/Graphics/RenderQueue.cs b/source/Vignette/Graphics/RenderQueue.cs index 5466303..1ba6f55 100644 --- a/source/Vignette/Graphics/RenderQueue.cs +++ b/source/Vignette/Graphics/RenderQueue.cs @@ -20,8 +20,8 @@ public sealed class RenderQueue : IReadOnlyCollection /// public int Count => renderables.Count; + private readonly List renderOrders = new(); private readonly List renderables = new(); - private readonly List renderOrders = new(); /// /// Creates a new render queue. @@ -34,9 +34,9 @@ public RenderQueue() /// Enqueues a to this queue. /// /// The projector used. - /// The model used. + /// The model used. /// The render object to be enqueued. - public void Enqueue(IProjector projector, IWorld world, RenderObject renderObject) + public void Enqueue(IProjector projector, ISpatialObject spatial, RenderObject renderObject) { if ((projector.Groups & renderObject.Groups) != 0) { @@ -53,9 +53,9 @@ public void Enqueue(IProjector projector, IWorld world, RenderObject renderObjec int renderable = renderables.Count; int materialID = renderObject.Material.GetMaterialID(); - float distance = Vector3.Distance((renderObject.Bounds.Center * world.Scale) + world.Position, projector.Position); + float distance = Vector3.Distance((renderObject.Bounds.Center * spatial.Scale) + spatial.Position, projector.Position); - renderables.Add(new RenderData(projector, world, renderObject)); + renderables.Add(new RenderData(projector, spatial, renderObject)); renderOrders.Add(new(renderable, materialID, distance)); } @@ -88,10 +88,10 @@ private struct Enumerator : IEnumerator public RenderData Current { get; private set; } private int index; + private readonly IReadOnlyList renderOrders; private readonly IReadOnlyList renderables; - private readonly IReadOnlyList renderOrders; - public Enumerator(IReadOnlyList renderOrders, IReadOnlyList renderables) + public Enumerator(IReadOnlyList renderOrders, IReadOnlyList renderables) { this.renderables = renderables; this.renderOrders = renderOrders; @@ -125,20 +125,20 @@ public readonly void Dispose() readonly object IEnumerator.Current => Current; } - private readonly struct RenderOrder : IEquatable, IComparable + private readonly struct RenderKey : IEquatable, IComparable { public int Renderable { get; } public int MaterialID { get; } public float Distance { get; } - public RenderOrder(int renderable, int materialID, float distance) + public RenderKey(int renderable, int materialID, float distance) { Distance = distance; Renderable = renderable; MaterialID = materialID; } - public readonly int CompareTo(RenderOrder other) + public readonly int CompareTo(RenderKey other) { if (Equals(other)) { @@ -155,14 +155,14 @@ public readonly int CompareTo(RenderOrder other) return MaterialID.CompareTo(other.MaterialID); } - public readonly bool Equals(RenderOrder other) + public readonly bool Equals(RenderKey other) { return Renderable.Equals(other.Renderable) && MaterialID.Equals(other.MaterialID) && Distance.Equals(other.Distance); } public override readonly bool Equals([NotNullWhen(true)] object? obj) { - return obj is RenderOrder order && Equals(order); + return obj is RenderKey order && Equals(order); } public override readonly int GetHashCode() @@ -170,32 +170,32 @@ public override readonly int GetHashCode() return HashCode.Combine(Renderable, MaterialID, Distance); } - public static bool operator ==(RenderOrder left, RenderOrder right) + public static bool operator ==(RenderKey left, RenderKey right) { return left.Equals(right); } - public static bool operator !=(RenderOrder left, RenderOrder right) + public static bool operator !=(RenderKey left, RenderKey right) { return !(left == right); } - public static bool operator <(RenderOrder left, RenderOrder right) + public static bool operator <(RenderKey left, RenderKey right) { return left.CompareTo(right) < 0; } - public static bool operator <=(RenderOrder left, RenderOrder right) + public static bool operator <=(RenderKey left, RenderKey right) { return left.CompareTo(right) <= 0; } - public static bool operator >(RenderOrder left, RenderOrder right) + public static bool operator >(RenderKey left, RenderKey right) { return left.CompareTo(right) > 0; } - public static bool operator >=(RenderOrder left, RenderOrder right) + public static bool operator >=(RenderKey left, RenderKey right) { return left.CompareTo(right) >= 0; } @@ -208,11 +208,11 @@ internal static class RenderQueueExtensions /// Begins a rendering context. /// /// The render queue. - /// The projector used. - /// The model used. + /// The projector. + /// The spatial object. /// A new render context. - internal static RenderContext Begin(this RenderQueue queue, IProjector projector, IWorld world) + internal static RenderContext Begin(this RenderQueue queue, IProjector projector, ISpatialObject spatial) { - return new RenderContext(queue, projector, world); + return new RenderContext(queue, projector, spatial); } } diff --git a/source/Vignette/Graphics/Renderer.cs b/source/Vignette/Graphics/Renderer.cs index 79ae4eb..6aa3c8c 100644 --- a/source/Vignette/Graphics/Renderer.cs +++ b/source/Vignette/Graphics/Renderer.cs @@ -32,13 +32,13 @@ public sealed class Renderer /// public Sampler SamplerAniso4x { get; } - private readonly GraphicsBuffer ubo; + private readonly GraphicsBuffer buffer; private readonly GraphicsDevice device; private readonly Dictionary caches = new(); - internal Renderer(GraphicsDevice device) + public Renderer(GraphicsDevice device) { - ubo = device.CreateBuffer(BufferType.Uniform, 3, true); + buffer = device.CreateBuffer(BufferType.Uniform, 3, true); Span whitePixel = stackalloc byte[] { 255, 255, 255, 255 }; @@ -52,16 +52,6 @@ internal Renderer(GraphicsDevice device) this.device = device; } - /// - /// Draws a single to . - /// - /// The renderable to draw. - /// The target to draw to. A value of to draw to the backbuffer. - public void Draw(RenderData renderable, RenderTarget? target = null) - { - Draw(new[] { renderable }, target); - } - /// /// Draws to . /// @@ -74,14 +64,14 @@ public void Draw(IEnumerable renderables, RenderTarget? target = nul foreach (var data in renderables) { - using (var mvp = ubo.Map(MapMode.Write)) + using (var mvp = buffer.Map(MapMode.Write)) { mvp[0] = data.Projector.ProjMatrix; mvp[1] = data.Projector.ViewMatrix; - mvp[2] = data.World.WorldMatrix; + mvp[2] = data.Spatial.Matrix; } - device.SetUniformBuffer(ubo, Effect.GLOBAL_TRANSFORM_ID); + device.SetUniformBuffer(buffer, Effect.GLOBAL_TRANSFORM_ID); if (target is not null) { diff --git a/source/Vignette/Node.cs b/source/Vignette/Node.cs index 908ded8..755e6f4 100644 --- a/source/Vignette/Node.cs +++ b/source/Vignette/Node.cs @@ -9,7 +9,9 @@ using System.IO; using System.Linq; using System.Numerics; +using Jint.Native; using Vignette.Graphics; +using Vignette.Scripting; namespace Vignette; @@ -17,7 +19,7 @@ namespace Vignette; /// The base class of everything that resides inside the node graph. It can be a child of /// another and can contain its own children s. /// -public class Node : IWorld, INotifyCollectionChanged, ICollection, IEquatable +public class Node : ScriptObject, IPointObject, INotifyCollectionChanged, ICollection, IEquatable { /// /// The 's unique identifier. @@ -27,11 +29,13 @@ public class Node : IWorld, INotifyCollectionChanged, ICollection, IEquata /// /// The 's name. /// - public string Name { get; } + [ScriptVisible] + public string Name { get; set; } /// /// The depth of this relative to the root. /// + [ScriptVisible] public int Depth { get; private set; } /// @@ -39,76 +43,49 @@ public class Node : IWorld, INotifyCollectionChanged, ICollection, IEquata /// public int Count => nodes.Count; - /// - /// The 's services. - /// - public virtual IServiceLocator Services => Parent is not null ? Parent.Services : throw new InvalidOperationException("Services are unavailable"); - /// /// The parent . /// + [ScriptVisible] public Node? Parent { get; private set; } /// /// The node's position. /// + [ScriptVisible] public Vector3 Position { get; set; } /// /// The node's rotation. /// + [ScriptVisible] public Vector3 Rotation { get; set; } /// - /// The node's scaling. - /// - public Vector3 Scale { get; set; } = Vector3.One; - - /// - /// The node's shearing. + /// Called when the 's children has been changed. /// - public Vector3 Shear - { - get => new(shear[0, 1], shear[0, 2], shear[1, 2]); - set - { - shear[0, 1] = value.X; - shear[0, 2] = value.Y; - shear[1, 2] = value.Z; - } - } + public event NotifyCollectionChangedEventHandler? CollectionChanged; /// /// The node's local matrix. /// - protected virtual Matrix4x4 LocalMatrix => shear * Matrix4x4.CreateScale(Scale) * Matrix4x4.CreateFromYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z) * Matrix4x4.CreateTranslation(Position); - - /// - /// The node's world matrix. - /// - protected virtual Matrix4x4 WorldMatrix => Parent is not IWorld provider ? LocalMatrix : provider.LocalMatrix * LocalMatrix; - - /// - /// Called when the 's children has been changed. - /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; + protected virtual Matrix4x4 Matrix => Matrix4x4.CreateFromYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z) * Matrix4x4.CreateTranslation(Position); - private Matrix4x4 shear = Matrix4x4.Identity; - private readonly Dictionary nodes = new(); + private readonly List nodes = new(); /// /// Creates a new . /// - /// The optional name for this . - public Node(string? name = null) - : this(Guid.NewGuid(), name) + /// The name for this . + public Node() + : this(Guid.NewGuid()) { } - private Node(Guid id, string? name = null) + private Node(Guid id) { Id = id; - Name = name ?? id.ToString(); + Name = id.ToString(); } /// @@ -116,6 +93,7 @@ private Node(Guid id, string? name = null) /// protected virtual void Enter() { + Invoke(enter); } /// @@ -123,6 +101,7 @@ protected virtual void Enter() /// protected virtual void Leave() { + Invoke(leave); } /// @@ -173,7 +152,7 @@ public bool Remove(Node node) /// The number of removed children. public int RemoveRange(Predicate predicate) { - var selected = nodes.Values.Where(n => predicate(n)).ToArray(); + var selected = nodes.Where(n => predicate(n)).ToArray(); foreach (var node in selected) { @@ -212,7 +191,7 @@ public int RemoveRange(IEnumerable nodes) /// public void Clear() { - var copy = nodes.Values.ToArray(); + var copy = nodes.ToArray(); foreach (var node in copy) { @@ -229,7 +208,7 @@ public void Clear() /// if the is a child of this node or if not. public bool Contains(Node node) { - return nodes.ContainsValue(node); + return nodes.Contains(node); } /// @@ -248,29 +227,6 @@ public Node GetRoot() return current; } - /// - /// Gets the nearest node. - /// - /// The node type to search for. - /// The nearest node. - public T? GetNearest() - where T : Node - { - var current = this; - - while (current.Parent is not null) - { - current = current.Parent; - - if (current is T) - { - break; - } - } - - return current as T; - } - /// /// Gets the node from the given path. /// @@ -293,14 +249,28 @@ public Node GetNode(string path) var current = uri.IsAbsoluteUri ? GetRoot() : this; string[] cm = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped).Split(Path.AltDirectorySeparatorChar, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var lookup = new Dictionary(); + foreach (string part in cm) { - if (!current.nodes.ContainsKey(part)) + lookup.Clear(); + + foreach (var node in current) + { + if (lookup.ContainsKey(node.Name)) + { + continue; + } + + lookup[node.Name] = node; + } + + if (!lookup.TryGetValue(part, out var next)) { throw new KeyNotFoundException($"The node \"{part}\" was not found."); } - current = current.nodes[part]; + current = next; } return current; @@ -339,7 +309,7 @@ public IEnumerable GetNodes() public IEnumerator GetEnumerator() { - return nodes.Values.GetEnumerator(); + return nodes.GetEnumerator(); } public bool Equals(Node? node) @@ -389,7 +359,7 @@ private void add(Node node) throw new ArgumentException("Cannot add a node that already has a parent.", nameof(node)); } - if (nodes.ContainsKey(node.Name)) + if (Contains(node)) { throw new ArgumentException($"There is already a child with the name \"{node.Name}\".", nameof(node)); } @@ -397,14 +367,14 @@ private void add(Node node) node.Depth = Depth + 1; node.Parent = this; - nodes.Add(node.Name, node); + nodes.Add(node); node.Enter(); } private bool remove(Node node) { - if (!nodes.ContainsKey(node.Name)) + if (!nodes.Contains(node)) { return false; } @@ -414,14 +384,14 @@ private bool remove(Node node) node.Depth = 0; node.Parent = null; - nodes.Remove(node.Name); + nodes.Remove(node); return true; } void ICollection.CopyTo(Node[] array, int arrayIndex) { - nodes.Values.CopyTo(array, arrayIndex); + nodes.CopyTo(array, arrayIndex); } IEnumerator IEnumerable.GetEnumerator() @@ -430,9 +400,10 @@ IEnumerator IEnumerable.GetEnumerator() } bool ICollection.IsReadOnly => false; - Matrix4x4 IWorld.LocalMatrix => LocalMatrix; - Matrix4x4 IWorld.WorldMatrix => WorldMatrix; + Matrix4x4 IPointObject.Matrix => Parent is not ISpatialObject parent ? Matrix : parent.Matrix * Matrix; private const string node_scheme = "node"; + private static readonly JsValue enter = new JsString("enter"); + private static readonly JsValue leave = new JsString("leave"); private static readonly NotifyCollectionChangedEventArgs reset_args = new(NotifyCollectionChangedAction.Reset); } diff --git a/source/Vignette/Scripting/ScriptModuleLoader.cs b/source/Vignette/Scripting/ScriptModuleLoader.cs new file mode 100644 index 0000000..10cdf01 --- /dev/null +++ b/source/Vignette/Scripting/ScriptModuleLoader.cs @@ -0,0 +1,105 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System; +using System.IO; +using Esprima; +using Esprima.Ast; +using Jint; +using Jint.Runtime; +using Jint.Runtime.Modules; +using Sekai.Storages; + +namespace Vignette.Scripting; + +public sealed class ScriptModuleLoader : IModuleLoader +{ + private readonly Storage storage; + private static readonly Uri baseUri = new("files:///extensions/"); + + public ScriptModuleLoader(Storage storage) + { + this.storage = storage; + } + + public Module LoadModule(Engine engine, ResolvedSpecifier resolved) + { + if (resolved.Type != SpecifierType.RelativeOrAbsolute) + { + throw new NotSupportedException("Cannot load from bare specifiers."); + } + + if (resolved.Uri is null) + { + throw new InvalidOperationException("Module has no resolved URI."); + } + + string path = Uri.UnescapeDataString(resolved.Uri.AbsolutePath); + + if (!storage.Exists(path)) + { + throw new ArgumentException("Module Not Found: ", resolved.Specifier); + } + + using var stream = storage.Open(path, FileMode.Open, FileAccess.Read); + using var reader = new StreamReader(stream); + + string code = reader.ReadToEnd(); + + try + { + return new JavaScriptParser().ParseModule(code, resolved.Uri.LocalPath); + } + catch (ParserException ex) + { + throw new JavaScriptException(engine.Construct("SyntaxError", $"Error while loading module: error in module '{resolved.Uri.LocalPath}': {ex.Error}")); + } + catch (Exception) + { + throw new JavaScriptException($"Could not load module {resolved.Uri.LocalPath}"); + } + } + + public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier) + { + if (string.IsNullOrEmpty(specifier)) + { + throw new ModuleResolutionException("Invalid Module Specifier.", specifier, referencingModuleLocation); + } + + Uri resolved; + + if (Uri.TryCreate(specifier, UriKind.Absolute, out _)) + { + throw new ModuleResolutionException("Absolute Paths are not permitted.", specifier, referencingModuleLocation); + } + else if (isRelative(specifier)) + { + resolved = new Uri(baseUri, specifier); + } + else + { + return new ResolvedSpecifier(specifier, specifier, null, SpecifierType.Bare); + } + + if (resolved.IsFile) + { + if (resolved.UserEscaped) + { + throw new ModuleResolutionException("Invalid Module Specifier.", specifier, referencingModuleLocation); + } + + if (!Path.HasExtension(resolved.LocalPath)) + { + throw new ModuleResolutionException("Unsupported Directory Import.", specifier, referencingModuleLocation); + } + } + + return new ResolvedSpecifier(specifier, resolved.AbsoluteUri, resolved, SpecifierType.RelativeOrAbsolute); + } + + private static bool isRelative(string specifier) + { + return specifier[0] is '.' or '/'; + } +} diff --git a/source/Vignette/Scripting/ScriptObject.cs b/source/Vignette/Scripting/ScriptObject.cs new file mode 100644 index 0000000..5518c05 --- /dev/null +++ b/source/Vignette/Scripting/ScriptObject.cs @@ -0,0 +1,71 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System; +using Jint; +using Jint.Native; +using Jint.Native.Object; + +namespace Vignette.Scripting; + +/// +/// An object that can be scripted in JavaScript. +/// +public abstract class ScriptObject : IScriptObject +{ + private readonly WeakReference jsObject = new(null); + + /// + /// Invokes a JavaScript function. + /// + /// The object implementing . + /// The property to call. + /// The arguments to pass to the function. + /// The result of calling the function. + public object? Invoke(JsValue property, params object?[] arguments) + { + if (jsObject.Target is not ObjectInstance obj) + { + return null; + } + + var target = obj.Get(property); + + if (target.IsUndefined()) + { + return null; + } + + return obj.Engine.Invoke(target, obj, arguments); + } + + /// + /// Invokes a JavaScript function. + /// + /// The type to cast the result as. + /// The object implementing . + /// The property to call. + /// The arguments to pass to the function. + /// The result of calling the function as . + public T? Invoke(JsValue property, params object?[] arguments) + { + return (T?)Invoke(property, arguments); + } + + ObjectInstance? IScriptObject.JSObject + { + get => jsObject.Target as ObjectInstance; + set => jsObject.Target = value; + } +} + +/// +/// Denotes that a the object can be scripted in JavaScript. +/// +internal interface IScriptObject +{ + /// + /// The JavaScript Object wrapping object. + /// + ObjectInstance? JSObject { set; get; } +} diff --git a/source/Vignette/Scripting/ScriptVisibleAttribute.cs b/source/Vignette/Scripting/ScriptVisibleAttribute.cs new file mode 100644 index 0000000..1c0c49e --- /dev/null +++ b/source/Vignette/Scripting/ScriptVisibleAttribute.cs @@ -0,0 +1,14 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System; + +namespace Vignette.Scripting; + +/// +/// Denotes that a given or member is visible in scripting. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] +internal sealed class ScriptVisibleAttribute : Attribute +{ +} diff --git a/source/Vignette/ServiceLocator.cs b/source/Vignette/ServiceLocator.cs index 5da32b5..4306a93 100644 --- a/source/Vignette/ServiceLocator.cs +++ b/source/Vignette/ServiceLocator.cs @@ -3,119 +3,231 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Vignette; /// -/// A collection of services. +/// Locates and resolves registered services. /// -public sealed class ServiceLocator : IServiceLocator, IServiceProvider +public sealed class ServiceLocator : IServiceProvider { - private readonly Dictionary services = new(); - /// - /// Adds a service to this locator. + /// An empty . /// - /// The type of service. - /// The service instance. - /// Thrown when is already added to this locator. - /// Thrown when cannot be assigned to . - public void Add(Type type, object instance) + public static readonly ServiceLocator Empty = new(ImmutableDictionary.Empty); + + private readonly IReadOnlyDictionary services; + + private ServiceLocator(IReadOnlyDictionary services) { - if (services.ContainsKey(type)) - { - throw new ArgumentException($"{type} already exists in this locator.", nameof(type)); - } + this.services = services; + } - if (!instance.GetType().IsAssignableTo(type)) + /// + /// Resolves the service of a given type. + /// + /// The object type to resolve. + /// Whether the service is required or not. + /// The service object of the given type or when is false and the service is not found. + /// Thrown when is true and the service is not found. + public object? Resolve(Type type, [DoesNotReturnIf(true)] bool required = true) + { + if (!services.TryGetValue(type, out object? instance) && required) { - throw new InvalidCastException($"The {nameof(instance)} cannot be casted to {type}."); + throw new ServiceNotFoundException(type); } - services.Add(type, instance); + return instance; } /// - /// Adds a service to this locator. + /// Resolves the service of a given type. /// - /// The service instance. - /// The type of service. - /// Thrown when is already added to this locator. - /// Thrown when cannot be assigned to . - public void Add(T instance) + /// The object type to resolve. + /// Whether the service is required or not. + /// The service object of type or when is false and the service is not found. + /// Thrown when is true and the service is not found. + public T? Resolve([DoesNotReturnIf(true)] bool required = true) where T : class { - Add(typeof(T), instance); + return Unsafe.As(Resolve(typeof(T), required)); } /// - /// Removes a service from this locator. + /// Attempts to resolve a service. /// - /// The service type to remove. - /// true when the service is removed. false otherwise. - public bool Remove(Type type) + /// The type of service to resolve. + /// The resolved service or if not found. + /// if the service has been resolved. Otherwise, . + public bool TryResolve(Type type, [NotNullWhen(true)] out object? service) { - return services.Remove(type); + service = Resolve(type, false); + return service is not null; } /// - /// Removes a service from this locator. + /// Attempts to resolve the . /// - /// The service type to remove. - /// true when the service is removed. false otherwise. - public bool Remove() + /// The type of service to resolve. + /// The resolved service or if not found. + /// if the service has been resolved. Otherwise, . + public bool TryResolve([NotNullWhen(true)] out T? service) where T : class { - return Remove(typeof(T)); + service = Resolve(false); + return service is not null; } - public object? Get(Type type, [DoesNotReturnIf(true)] bool required = true) + /// + /// Creates a registry to allow registering services. + /// + public static IServiceRegistry CreateRegistry() { - if (!services.TryGetValue(type, out object? instance) && required) + return new ServiceRegistry(); + } + + object? IServiceProvider.GetService(Type type) => Resolve(type, false); + + private sealed class ServiceRegistry : IServiceRegistry + { + private readonly ImmutableDictionary.Builder instances = ImmutableDictionary.CreateBuilder(); + + public IServiceRegistry Register(Type type, object instance) { - throw new ServiceNotFoundException(type); + if (type.IsValueType) + { + throw new ArgumentException($"{type} is a value type.", nameof(type)); + } + + if (instances.ContainsKey(type)) + { + throw new ArgumentException($"{type} already exists in this locator.", nameof(type)); + } + + if (!instance.GetType().IsAssignableTo(type)) + { + throw new InvalidCastException($"The {nameof(instance)} cannot be casted to {type}."); + } + + instances.Add(type, instance); + + return this; } - return instance; - } + public IServiceRegistry Register(T instance) + where T : notnull + { + return Register(typeof(T), instance); + } + public IServiceRegistry Register() + where T : notnull, new() + { + return Register(new T()); + } - public T? Get([DoesNotReturnIf(true)] bool required = true) - where T : class - { - return Unsafe.As(Get(typeof(T), required)); - } + public IServiceRegistry Register(Func creator) + where T : notnull + { + var type = typeof(T); + + if (instances.ContainsKey(type)) + { + throw new ArgumentException($"{type} already exists in this locator.", nameof(T)); + } - object? IServiceProvider.GetService(Type type) => Get(type, false); + instances.Add(type, (Func)(() => creator())); + + return this; + } + + public ServiceLocator Build() + { + foreach (var pair in instances.ToImmutableDictionary()) + { + if (pair.Value is Func creator) + { + instances[pair.Key] = creator(); + } + } + + var services = new ServiceLocator(instances.ToImmutableDictionary()); + + foreach (object obj in instances.Values) + { + if (obj is not IServiceObject service) + { + continue; + } + + service.Initialize(services); + } + + return services; + } + } } /// -/// An interface for objects capable of locating services. +/// Provides a mechanism for registering services which can then later be compiled. /// -public interface IServiceLocator +public interface IServiceRegistry { /// - /// Gets the service of a given type. + /// Registers a service. /// - /// The object type to resolve. - /// Whether the service is required or not. - /// The service object of the given type or when is false and the service is not found. - /// Thrown when is true and the service is not found. - object? Get(Type type, [DoesNotReturnIf(true)] bool required = true); + /// The type of service. + /// The service instance. + /// Thrown when is already added. + /// Thrown when cannot be assigned to . + IServiceRegistry Register(Type type, object instance); /// - /// Gets the service of a given type. + /// Registers a . /// - /// The object type to resolve. - /// Whether the service is required or not. - /// The service object of type or when is false and the service is not found. - /// Thrown when is true and the service is not found. - T? Get([DoesNotReturnIf(true)] bool required = true) where T : class; + /// The service instance. + /// The type of service. + /// Thrown when is already added. + /// Thrown when cannot be assigned to . + IServiceRegistry Register(T instance) where T : notnull; + + /// + /// Registers a . + /// + /// The type of service. + IServiceRegistry Register() where T : notnull, new(); + + /// + /// Lazily registers a . + /// + /// The type to register. + /// The creation function. + /// Thrown when is already added + IServiceRegistry Register(Func creator) where T : notnull; + + /// + /// Compiles registered services to a . + /// + /// The built service locator. + ServiceLocator Build(); +} + +/// +/// An object that resolves services. +/// +internal interface IServiceObject +{ + /// + /// Initializes the . + /// + /// The service locator. + void Initialize(ServiceLocator services); } /// -/// Exception thrown when fails to locate a required service of a given type. +/// Exception thrown when fails to locate a required service of a given type. /// public sealed class ServiceNotFoundException : Exception { diff --git a/source/Vignette/Vignette.csproj b/source/Vignette/Vignette.csproj index 0bd80f1..dce48c1 100644 --- a/source/Vignette/Vignette.csproj +++ b/source/Vignette/Vignette.csproj @@ -6,7 +6,8 @@ - + + diff --git a/source/Vignette/VignetteGame.cs b/source/Vignette/VignetteGame.cs index 13eef01..9c65f06 100644 --- a/source/Vignette/VignetteGame.cs +++ b/source/Vignette/VignetteGame.cs @@ -2,54 +2,42 @@ // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. using System; +using Jint; using Sekai; -using Vignette.Audio; -using Vignette.Content; using Vignette.Graphics; +using Vignette.Scripting; namespace Vignette; public sealed class VignetteGame : Game { - private Window root = null!; - private Camera camera = null!; - private Renderer renderer = null!; - private AudioManager audio = null!; - private ContentManager content = null!; - private ServiceLocator services = null!; - - public override void Load() - { - audio = new(Audio); - content = new(Storage); - content.Add(new ShaderLoader(), ".hlsl"); - content.Add(new TextureLoader(Graphics), ".png", ".jpg", ".jpeg", ".bmp", ".gif"); - - renderer = new(Graphics); + private readonly World root; + private readonly Engine engine; + private readonly Renderer renderer; - services = new(); - services.Add(audio); - services.Add(content); + public VignetteGame(VignetteGameOptions options) + : base(options) + { + options.Engine.Strict = true; + options.Engine.Modules.ModuleLoader = new ScriptModuleLoader(Storage); - root = new(services) - { - (camera = new Camera { ProjectionMode = CameraProjectionMode.OrthographicOffCenter }) - }; + root = new World(); + engine = new Engine(options.Engine); + renderer = new Renderer(Graphics); } - public override void Draw() + protected override void Draw() { root.Draw(renderer); } - public override void Update(TimeSpan elapsed) + protected override void Update(TimeSpan elapsed) { - camera.ViewSize = Window.Size; - audio.Update(); + engine.Advanced.ProcessTasks(); root.Update(elapsed); } - public override void Unload() + protected override void Unload() { root.Clear(); } diff --git a/source/Vignette/VignetteGameOptions.cs b/source/Vignette/VignetteGameOptions.cs new file mode 100644 index 0000000..4dbc6da --- /dev/null +++ b/source/Vignette/VignetteGameOptions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System.Reflection; +using Jint; +using Jint.Runtime.Interop; +using Sekai; +using Vignette.Scripting; + +namespace Vignette; + +public class VignetteGameOptions : GameOptions +{ + /// + /// The scripting engine options. + /// + public Options Engine = new(); +} + +public static class VignetteGameOptionsExtensions +{ + /// + /// Enable use of scripts. + /// + /// + public static void UseScripts(this VignetteGameOptions options) + { + options.Engine.Interop.TypeResolver = typeResolver; + options.Engine.Interop.WrapObjectHandler = wrapObjectHandler; + } + + private static readonly WrapObjectDelegate wrapObjectHandler = static (Engine engine, object target) => + { + if (target is IScriptObject wrapped) + { + return (ObjectWrapper)(wrapped.JSObject ??= new ObjectWrapper(engine, target)); + } + else + { + return new ObjectWrapper(engine, target); + } + }; + + private static readonly TypeResolver typeResolver = new() + { + MemberFilter = m => + { + return m.GetCustomAttribute() is not null; + }, + }; +} diff --git a/source/Vignette/Window.cs b/source/Vignette/Window.cs deleted file mode 100644 index 50832ef..0000000 --- a/source/Vignette/Window.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Cosyne -// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. - -namespace Vignette; - -/// -/// The root of . -/// -public sealed class Window : World -{ - public override IServiceLocator Services { get; } - - internal Window(IServiceLocator services) - { - Services = services; - } -} diff --git a/source/Vignette/World.cs b/source/Vignette/World.cs index cda0a6e..2b04ee0 100644 --- a/source/Vignette/World.cs +++ b/source/Vignette/World.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using System.Numerics; using Sekai.Mathematics; using Vignette.Collections; using Vignette.Graphics; @@ -15,62 +14,62 @@ namespace Vignette; /// /// A that presents and processes its children. /// -public class World : Behavior +public class World : Node { - protected override Matrix4x4 WorldMatrix => LocalMatrix; - private readonly SortedFilteredCollection behaviors = new ( - Comparer.Default, - (node) => node.Enabled, - (node, handler) => node.OrderChanged += handler, - (node, handler) => node.OrderChanged -= handler, - (node, handler) => node.EnabledChanged += handler, - (node, handler) => node.EnabledChanged -= handler + Comparer.Default, + static (node) => node.Enabled, + static (node, handler) => node.OrderChanged += handler, + static (node, handler) => node.OrderChanged -= handler, + static (node, handler) => node.EnabledChanged += handler, + static (node, handler) => node.EnabledChanged -= handler ); private readonly SortedFilteredCollection drawables = new ( - Comparer.Default, - (node) => node.Visible, - (node, handler) => node.OrderChanged += handler, - (node, handler) => node.OrderChanged -= handler, - (node, handler) => node.VisibleChanged += handler, - (node, handler) => node.VisibleChanged -= handler - ); - - private readonly SortedFilteredCollection worlds = new - ( - Comparer.Default, - (node) => node.Enabled, - (node, handler) => node.OrderChanged += handler, - (node, handler) => node.OrderChanged -= handler, - (node, handler) => node.EnabledChanged += handler, - (node, handler) => node.EnabledChanged -= handler + Comparer.Default, + static (node) => node.Visible, + static (node, handler) => node.OrderChanged += handler, + static (node, handler) => node.OrderChanged -= handler, + static (node, handler) => node.VisibleChanged += handler, + static (node, handler) => node.VisibleChanged -= handler ); + private readonly List worlds = new(); private readonly List lights = new(); private readonly List cameras = new(); - private readonly RenderQueue renderQueue = new(); - private readonly Queue behaviorLoadQueue = new(); - private readonly Queue behaviorUnloadQueue = new(); + private readonly Queue enterQueue = new(); + private readonly Queue leaveQueue = new(); public World() { CollectionChanged += handleCollectionChanged; } - public override void Update(TimeSpan elapsed) + public void Update(TimeSpan elapsed) { - while (behaviorLoadQueue.TryDequeue(out var node)) + while (enterQueue.TryDequeue(out var behavior)) { - node.Load(); + behavior.Load(); + behaviors.Add(behavior); + + if (behavior is Drawable drawable) + { + drawables.Add(drawable); + } } - while (behaviorUnloadQueue.TryDequeue(out var node)) + while (leaveQueue.TryDequeue(out var behavior)) { - node.Unload(); + behavior.Unload(); + behaviors.Remove(behavior); + + if (behavior is Drawable drawable) + { + drawables.Remove(drawable); + } } foreach (var behavior in behaviors) @@ -86,8 +85,6 @@ public void Draw(Renderer renderer) world.Draw(renderer); } - // Shadow Map Pass - foreach (var camera in cameras) { foreach (var light in lights) @@ -101,15 +98,13 @@ public void Draw(Renderer renderer) foreach (var drawable in drawables) { - drawable.Draw(renderQueue.Begin(light, drawable)); + drawable.Draw(renderQueue.Begin(camera, drawable)); } renderer.Draw(renderQueue); } } - // Lighting Pass - foreach (var camera in cameras) { renderQueue.Clear(); @@ -127,7 +122,7 @@ private void handleCollectionChanged(object? sender, NotifyCollectionChangedEven { if (args.Action == NotifyCollectionChangedAction.Add) { - foreach (var node in args.NewItems!.OfType()) + foreach (var node in args.NewItems!.Cast()) { load(node); } @@ -135,7 +130,7 @@ private void handleCollectionChanged(object? sender, NotifyCollectionChangedEven if (args.Action == NotifyCollectionChangedAction.Remove) { - foreach (var node in args.OldItems!.OfType()) + foreach (var node in args.OldItems!.Cast()) { unload(node); } @@ -152,20 +147,23 @@ private void handleCollectionChanged(object? sender, NotifyCollectionChangedEven private void load(Node node) { - foreach (var child in node.GetNodes()) + foreach (var child in node) { load(child); } if (node is Behavior behavior) { - behaviors.Add(behavior); - behaviorLoadQueue.Enqueue(behavior); + enterQueue.Enqueue(behavior); } - if (node is Drawable drawable) + if (node is World world) + { + worlds.Add(world); + } + else { - drawables.Add(drawable); + node.CollectionChanged += handleCollectionChanged; } if (node is Light light) @@ -177,33 +175,27 @@ private void load(Node node) { cameras.Add(camera); } - - if (node is World world) - { - worlds.Add(world); - } - else - { - node.CollectionChanged += handleCollectionChanged; - } } private void unload(Node node) { - foreach (var child in node.GetNodes()) + foreach (var child in node) { unload(child); } if (node is Behavior behavior) { - behaviors.Remove(behavior); - behaviorUnloadQueue.Enqueue(behavior); + leaveQueue.Enqueue(behavior); } - if (node is Drawable drawable) + if (node is World world) { - drawables.Remove(drawable); + worlds.Add(world); + } + else + { + node.CollectionChanged -= handleCollectionChanged; } if (node is Light light) @@ -215,14 +207,5 @@ private void unload(Node node) { cameras.Remove(camera); } - - if (node is World world) - { - worlds.Add(world); - } - else - { - node.CollectionChanged -= handleCollectionChanged; - } } }