.
This commit is contained in:
@@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatchConsole", "Do
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatchWPF", "DoomDeathmatchWPF\DoomDeathmatchWPF.csproj", "{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatchWPF", "DoomDeathmatchWPF\DoomDeathmatchWPF.csproj", "{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineTests", "EngineTests\EngineTests.csproj", "{CC28C26C-0998-4C13-8855-978658B8B0D6}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -30,5 +32,9 @@ Global
|
|||||||
{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CC28C26C-0998-4C13-8855-978658B8B0D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC28C26C-0998-4C13-8855-978658B8B0D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CC28C26C-0998-4C13-8855-978658B8B0D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CC28C26C-0998-4C13-8855-978658B8B0D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="EngineTests" />
|
||||||
|
|
||||||
<PackageReference Include="OpenTK" Version="4.8.2"/>
|
<PackageReference Include="OpenTK" Version="4.8.2"/>
|
||||||
<PackageReference Include="Serilog" Version="4.1.0"/>
|
<PackageReference Include="Serilog" Version="4.1.0"/>
|
||||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Engine.Scene.Component;
|
|
||||||
|
|
||||||
public abstract class Component(GameObject gameObject)
|
|
||||||
{
|
|
||||||
public Guid Id { get; } = Guid.NewGuid();
|
|
||||||
|
|
||||||
public GameObject GameObject { get; } = gameObject;
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
using Engine.Scene.Component;
|
|
||||||
|
|
||||||
namespace Engine.Scene;
|
|
||||||
|
|
||||||
public sealed class GameObject
|
|
||||||
{
|
|
||||||
public Guid Id { get; } = Guid.NewGuid();
|
|
||||||
public Transform Transform { get; }
|
|
||||||
|
|
||||||
private readonly Queue<Action> _componentActions = new();
|
|
||||||
|
|
||||||
private readonly List<Component.Component> _components = new();
|
|
||||||
private readonly ISet<Type> _addedComponentTypes = new HashSet<Type>();
|
|
||||||
|
|
||||||
public GameObject()
|
|
||||||
{
|
|
||||||
AddComponent<Transform>();
|
|
||||||
UpdateComponents();
|
|
||||||
|
|
||||||
Transform = GetComponent<Transform>()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T? GetComponent<T>() where T : Component.Component
|
|
||||||
{
|
|
||||||
if (!_addedComponentTypes.Contains(typeof(T)))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return _components.OfType<T>().FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddComponent<T>(params object?[] args) where T : Component.Component
|
|
||||||
{
|
|
||||||
if (_addedComponentTypes.Contains(typeof(T)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var newArgs = new object?[args.Length + 1];
|
|
||||||
newArgs[0] = this;
|
|
||||||
for (var i = 0; i < args.Length; i++)
|
|
||||||
newArgs[i + 1] = args[i];
|
|
||||||
|
|
||||||
var component = (T?)Activator.CreateInstance(typeof(T), newArgs);
|
|
||||||
if (component == null)
|
|
||||||
throw new InvalidOperationException($"Failed to create component of type {typeof(T)}");
|
|
||||||
|
|
||||||
_componentActions.Enqueue(() =>
|
|
||||||
{
|
|
||||||
_components.Add(component);
|
|
||||||
_addedComponentTypes.Add(typeof(T));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveComponent<T>() where T : Component.Component
|
|
||||||
{
|
|
||||||
if (!_addedComponentTypes.Contains(typeof(T)) || typeof(T) == typeof(Transform))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var component = GetComponent<T>();
|
|
||||||
if (component == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_componentActions.Enqueue(() =>
|
|
||||||
{
|
|
||||||
_components.Remove(component);
|
|
||||||
_addedComponentTypes.Remove(typeof(T));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateComponents()
|
|
||||||
{
|
|
||||||
while (_componentActions.TryDequeue(out var action))
|
|
||||||
action();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
using System.Runtime.InteropServices;
|
using OpenTK.Graphics.OpenGL;
|
||||||
using System.Text;
|
|
||||||
using Engine.Renderer.Buffer;
|
|
||||||
using Engine.Renderer.Buffer.Vertex;
|
|
||||||
using Engine.Renderer.Shader;
|
|
||||||
using OpenTK.Graphics.OpenGL;
|
|
||||||
using OpenTK.Mathematics;
|
using OpenTK.Mathematics;
|
||||||
using OpenTK.Windowing.Common;
|
using OpenTK.Windowing.Common;
|
||||||
using OpenTK.Windowing.Desktop;
|
using OpenTK.Windowing.Desktop;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
|
||||||
using Serilog.Sinks.SystemConsole.Themes;
|
using Serilog.Sinks.SystemConsole.Themes;
|
||||||
|
|
||||||
namespace Engine;
|
namespace Engine;
|
||||||
@@ -59,7 +53,7 @@ public sealed class Engine
|
|||||||
|
|
||||||
while (!_window.IsExiting)
|
while (!_window.IsExiting)
|
||||||
{
|
{
|
||||||
|
_window.Update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,5 @@ public interface ICamera
|
|||||||
{
|
{
|
||||||
public Matrix4 View { get; }
|
public Matrix4 View { get; }
|
||||||
public Matrix4 Projection { get; }
|
public Matrix4 Projection { get; }
|
||||||
|
public Vector2i ScreenSize { get; internal set; }
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,5 @@ public class ScreenspaceCamera : ICamera
|
|||||||
{
|
{
|
||||||
public Matrix4 View => Matrix4.Identity;
|
public Matrix4 View => Matrix4.Identity;
|
||||||
public Matrix4 Projection => Matrix4.Identity;
|
public Matrix4 Projection => Matrix4.Identity;
|
||||||
|
public Vector2i ScreenSize { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,16 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Engine.Renderer.Pixel;
|
|
||||||
using OpenTK.Graphics.OpenGL;
|
using OpenTK.Graphics.OpenGL;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
|
||||||
namespace Engine.Renderer;
|
namespace Engine.Renderer;
|
||||||
|
|
||||||
public class Renderer
|
internal static class Debug
|
||||||
{
|
{
|
||||||
internal Texture.Texture<Rgb8> TextureInternal => _framebuffer.TextureInternal;
|
public static void Setup()
|
||||||
|
|
||||||
private readonly Framebuffer.Framebuffer _framebuffer;
|
|
||||||
private readonly Queue<Action<Renderer>> _renderActions = new();
|
|
||||||
|
|
||||||
public Renderer(int width, int height)
|
|
||||||
{
|
|
||||||
InitializeOpenGl();
|
|
||||||
|
|
||||||
_framebuffer = new Framebuffer.Framebuffer(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeOpenGl()
|
|
||||||
{
|
{
|
||||||
GL.Enable(EnableCap.DebugOutput);
|
GL.Enable(EnableCap.DebugOutput);
|
||||||
GL.DebugMessageCallback(DebugCallback, IntPtr.Zero);
|
GL.DebugMessageCallback(DebugCallback, IntPtr.Zero);
|
||||||
|
|
||||||
GL.Enable(EnableCap.DepthTest);
|
|
||||||
|
|
||||||
GL.Enable(EnableCap.FramebufferSrgb);
|
|
||||||
|
|
||||||
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
|
||||||
GL.Enable(EnableCap.Blend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DebugCallback(DebugSource source, DebugType type, int id, DebugSeverity severity, int length,
|
private static void DebugCallback(DebugSource source, DebugType type, int id, DebugSeverity severity, int length,
|
||||||
@@ -65,8 +45,7 @@ public class Renderer
|
|||||||
_ => "Unknown"
|
_ => "Unknown"
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.Write(
|
logger.Write(GetLogLevel(severity),
|
||||||
GetLogLevel(severity),
|
|
||||||
"[OpenGL {TypePrefix}] [{Source}] {Message} (ID: 0x{Id:X8})",
|
"[OpenGL {TypePrefix}] [{Source}] {Message} (ID: 0x{Id:X8})",
|
||||||
typePrefix,
|
typePrefix,
|
||||||
sourcePrefix,
|
sourcePrefix,
|
||||||
@@ -86,22 +65,4 @@ public class Renderer
|
|||||||
_ => LogEventLevel.Debug
|
_ => LogEventLevel.Debug
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Commit(Action<Renderer> renderAction)
|
|
||||||
{
|
|
||||||
_renderActions.Enqueue(renderAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Render()
|
|
||||||
{
|
|
||||||
_framebuffer.Bind();
|
|
||||||
while (_renderActions.TryDequeue(out var renderAction))
|
|
||||||
renderAction(this);
|
|
||||||
_framebuffer.Unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Resize(int width, int height)
|
|
||||||
{
|
|
||||||
_framebuffer.Resize(width, height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
55
Engine/src/Renderer/Renderer.cs
Normal file
55
Engine/src/Renderer/Renderer.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Engine.Renderer.Pixel;
|
||||||
|
using Engine.Renderer.Shader;
|
||||||
|
using OpenTK.Graphics.OpenGL;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Engine.Renderer;
|
||||||
|
|
||||||
|
public class Renderer
|
||||||
|
{
|
||||||
|
internal Texture.Texture<Rgb8> TextureInternal => _framebuffer.TextureInternal;
|
||||||
|
|
||||||
|
private readonly Framebuffer.Framebuffer _framebuffer;
|
||||||
|
private readonly Queue<Action<Renderer>> _renderActions = new();
|
||||||
|
|
||||||
|
public Renderer(int width, int height)
|
||||||
|
{
|
||||||
|
InitializeOpenGl();
|
||||||
|
|
||||||
|
_framebuffer = new Framebuffer.Framebuffer(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeOpenGl()
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.Setup();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
GL.Enable(EnableCap.DepthTest);
|
||||||
|
|
||||||
|
GL.Enable(EnableCap.FramebufferSrgb);
|
||||||
|
|
||||||
|
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||||
|
GL.Enable(EnableCap.Blend);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Commit(Action<Renderer> renderAction)
|
||||||
|
{
|
||||||
|
_renderActions.Enqueue(renderAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Render()
|
||||||
|
{
|
||||||
|
_framebuffer.Bind();
|
||||||
|
while (_renderActions.TryDequeue(out var renderAction))
|
||||||
|
renderAction(this);
|
||||||
|
_framebuffer.Unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Resize(int width, int height)
|
||||||
|
{
|
||||||
|
_framebuffer.Resize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Engine/src/Scene/Component/BuiltIn/Camera.cs
Normal file
29
Engine/src/Scene/Component/BuiltIn/Camera.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Engine.Renderer.Camera;
|
||||||
|
using OpenTK.Mathematics;
|
||||||
|
|
||||||
|
namespace Engine.Scene.Component.BuiltIn;
|
||||||
|
|
||||||
|
public abstract class Camera(
|
||||||
|
float nearPlane,
|
||||||
|
float farPlane
|
||||||
|
) : Component, ICamera
|
||||||
|
{
|
||||||
|
public float AspectRatio { get; private set; } = 1;
|
||||||
|
public float NearPlane { get; set; } = nearPlane;
|
||||||
|
public float FarPlane { get; set; } = farPlane;
|
||||||
|
|
||||||
|
private Vector2i _screenSize = new(1, 1);
|
||||||
|
|
||||||
|
public abstract Matrix4 View { get; }
|
||||||
|
public abstract Matrix4 Projection { get; }
|
||||||
|
|
||||||
|
public Vector2i ScreenSize
|
||||||
|
{
|
||||||
|
get => _screenSize;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_screenSize = value;
|
||||||
|
AspectRatio = (float)value.X / value.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Engine/src/Scene/Component/BuiltIn/OrthographicCamera.cs
Normal file
27
Engine/src/Scene/Component/BuiltIn/OrthographicCamera.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using OpenTK.Mathematics;
|
||||||
|
|
||||||
|
namespace Engine.Scene.Component.BuiltIn;
|
||||||
|
|
||||||
|
public class OrthographicCamera(
|
||||||
|
float nearPlane = 0.1f,
|
||||||
|
float farPlane = 1000f,
|
||||||
|
float size = 10f,
|
||||||
|
OrthographicCamera.Axis axis = OrthographicCamera.Axis.Y
|
||||||
|
)
|
||||||
|
: Camera(nearPlane, farPlane)
|
||||||
|
{
|
||||||
|
public Axis FixedAxis { get; set; } = axis;
|
||||||
|
public float Size { get; set; } = size;
|
||||||
|
|
||||||
|
public override Matrix4 View => GameObject.Transform.TransformMatrix.Inverted();
|
||||||
|
|
||||||
|
public override Matrix4 Projection => FixedAxis == Axis.X
|
||||||
|
? Matrix4.CreateOrthographic(Size, Size / AspectRatio, NearPlane, FarPlane)
|
||||||
|
: Matrix4.CreateOrthographic(Size * AspectRatio, Size, NearPlane, FarPlane);
|
||||||
|
|
||||||
|
public enum Axis
|
||||||
|
{
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
using Engine.Renderer.Camera;
|
using Engine.Util;
|
||||||
using Engine.Util;
|
|
||||||
using OpenTK.Mathematics;
|
using OpenTK.Mathematics;
|
||||||
|
|
||||||
namespace Engine.Scene.Component;
|
namespace Engine.Scene.Component.BuiltIn;
|
||||||
|
|
||||||
public class PerspectiveCamera(
|
public class PerspectiveCamera(
|
||||||
GameObject gameObject,
|
float fieldOfView = 90.0f,
|
||||||
float aspectRatio,
|
float nearPlane = 0.1f,
|
||||||
float fieldOfView,
|
float farPlane = 1000f
|
||||||
float nearPlane,
|
)
|
||||||
float farPlane)
|
: Camera(nearPlane, farPlane)
|
||||||
: Component(gameObject), ICamera
|
|
||||||
{
|
{
|
||||||
public float AspectRatio { get; set; } = aspectRatio;
|
|
||||||
public float FieldOfView { get; set; } = fieldOfView;
|
public float FieldOfView { get; set; } = fieldOfView;
|
||||||
public float NearPlane { get; set; } = nearPlane;
|
|
||||||
public float FarPlane { get; set; } = farPlane;
|
|
||||||
|
|
||||||
public Matrix4 View
|
public override Matrix4 View
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@@ -30,5 +25,6 @@ public class PerspectiveCamera(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Matrix4 Projection => Matrix4.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlane, FarPlane);
|
public override Matrix4 Projection =>
|
||||||
|
Matrix4.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlane, FarPlane);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using OpenTK.Mathematics;
|
using OpenTK.Mathematics;
|
||||||
|
|
||||||
namespace Engine.Scene.Component;
|
namespace Engine.Scene.Component.BuiltIn;
|
||||||
|
|
||||||
public class Transform(GameObject gameObject) : Component(gameObject)
|
public class Transform : Component
|
||||||
{
|
{
|
||||||
public Vector3 Position { get; set; } = Vector3.Zero;
|
public Vector3 Position { get; set; } = Vector3.Zero;
|
||||||
public Quaternion Rotation { get; set; } = Quaternion.Identity;
|
public Quaternion Rotation { get; set; } = Quaternion.Identity;
|
||||||
@@ -15,5 +15,25 @@ public class Transform(GameObject gameObject) : Component(gameObject)
|
|||||||
|
|
||||||
public Matrix4 TransformMatrix => LocalTransformMatrix * ParentTransformMatrix;
|
public Matrix4 TransformMatrix => LocalTransformMatrix * ParentTransformMatrix;
|
||||||
|
|
||||||
private Matrix4 ParentTransformMatrix => Matrix4.Identity;
|
private Matrix4 ParentTransformMatrix
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var parent = GameObject.Scene?.Hierarchy.GetParent(GameObject);
|
||||||
|
return parent == null ? Matrix4.Identity : parent.Transform.TransformMatrix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transform Clone()
|
||||||
|
{
|
||||||
|
var clone = new Transform
|
||||||
|
{
|
||||||
|
Position = Position,
|
||||||
|
Rotation = Rotation,
|
||||||
|
Scale = Scale,
|
||||||
|
LocalScale = LocalScale
|
||||||
|
};
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
45
Engine/src/Scene/Component/Component.cs
Normal file
45
Engine/src/Scene/Component/Component.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
namespace Engine.Scene.Component;
|
||||||
|
|
||||||
|
public abstract class Component
|
||||||
|
{
|
||||||
|
public Guid Id { get; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public GameObject GameObject { get; internal set; }
|
||||||
|
|
||||||
|
internal virtual void Awake()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal virtual void Start()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal virtual void Update()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal virtual void Render()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal virtual void Destroy()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ComponentTypeExtensions
|
||||||
|
{
|
||||||
|
internal static Type GetComponentBaseType(this Type type)
|
||||||
|
{
|
||||||
|
var baseType = type.BaseType;
|
||||||
|
if (baseType == null || baseType == typeof(Component))
|
||||||
|
return type;
|
||||||
|
|
||||||
|
while (baseType.BaseType != null && baseType.BaseType != typeof(Component))
|
||||||
|
{
|
||||||
|
baseType = baseType.BaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseType;
|
||||||
|
}
|
||||||
|
}
|
||||||
136
Engine/src/Scene/GameObject.cs
Normal file
136
Engine/src/Scene/GameObject.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Engine.Scene.Component;
|
||||||
|
using Engine.Scene.Component.BuiltIn;
|
||||||
|
|
||||||
|
namespace Engine.Scene;
|
||||||
|
|
||||||
|
public sealed class GameObject
|
||||||
|
{
|
||||||
|
public Guid Id { get; } = Guid.NewGuid();
|
||||||
|
public Transform Transform { get; }
|
||||||
|
|
||||||
|
internal Scene? Scene { get; set; }
|
||||||
|
|
||||||
|
private readonly Queue<Action> _componentActions = new();
|
||||||
|
|
||||||
|
private readonly IList<Component.Component> _components = new List<Component.Component>();
|
||||||
|
private readonly ISet<Type> _addedComponentTypes = new HashSet<Type>();
|
||||||
|
|
||||||
|
public GameObject()
|
||||||
|
{
|
||||||
|
AddComponent<Transform>();
|
||||||
|
ProcessChanges();
|
||||||
|
|
||||||
|
Transform = GetComponent<Transform>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameObject(Transform transform)
|
||||||
|
{
|
||||||
|
AddComponent(transform.Clone());
|
||||||
|
ProcessChanges();
|
||||||
|
|
||||||
|
Transform = GetComponent<Transform>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Awake()
|
||||||
|
{
|
||||||
|
foreach (var component in _components)
|
||||||
|
component.Awake();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
foreach (var component in _components)
|
||||||
|
component.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
foreach (var component in _components)
|
||||||
|
component.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Render()
|
||||||
|
{
|
||||||
|
foreach (var component in _components)
|
||||||
|
component.Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Destroy()
|
||||||
|
{
|
||||||
|
foreach (var component in _components)
|
||||||
|
component.Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? GetComponent<T>() where T : Component.Component
|
||||||
|
{
|
||||||
|
return !HasComponent<T>() ? null : _components.OfType<T>().First();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddComponent<T>() where T : Component.Component, new()
|
||||||
|
{
|
||||||
|
var component = new T();
|
||||||
|
AddComponent(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddComponent<T>(params object?[] args) where T : Component.Component
|
||||||
|
{
|
||||||
|
var component = (T?)Activator.CreateInstance(
|
||||||
|
typeof(T),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance |
|
||||||
|
BindingFlags.OptionalParamBinding,
|
||||||
|
null,
|
||||||
|
args,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (component == null)
|
||||||
|
throw new InvalidOperationException($"Failed to create component of type {typeof(T)}");
|
||||||
|
|
||||||
|
AddComponent(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddComponent<T>(T component) where T : Component.Component
|
||||||
|
{
|
||||||
|
_componentActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
if (HasComponent<T>())
|
||||||
|
throw new ArgumentException($"GameObject already has component of type {typeof(T)}");
|
||||||
|
|
||||||
|
component.GameObject = this;
|
||||||
|
_components.Add(component);
|
||||||
|
_addedComponentTypes.Add(typeof(T).GetComponentBaseType());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveComponent<T>() where T : Component.Component
|
||||||
|
{
|
||||||
|
if (typeof(T) == typeof(Transform))
|
||||||
|
throw new ArgumentException("GameObject cannot remove Transform component");
|
||||||
|
|
||||||
|
_componentActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
if (!HasComponent<T>())
|
||||||
|
throw new ArgumentException($"GameObject does not have component of type {typeof(T)}");
|
||||||
|
|
||||||
|
var component = GetComponent<T>();
|
||||||
|
if (component == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_components.Remove(component);
|
||||||
|
_addedComponentTypes.Remove(typeof(T));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasComponent<T>() where T : Component.Component
|
||||||
|
{
|
||||||
|
var baseType = typeof(T).GetComponentBaseType();
|
||||||
|
return _addedComponentTypes.Contains(baseType);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ProcessChanges()
|
||||||
|
{
|
||||||
|
while (_componentActions.TryDequeue(out var action))
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
}
|
||||||
140
Engine/src/Scene/Hierarchy.cs
Normal file
140
Engine/src/Scene/Hierarchy.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Engine.Util;
|
||||||
|
|
||||||
|
namespace Engine.Scene;
|
||||||
|
|
||||||
|
public class Hierarchy<T> : IEnumerable<T>
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
private readonly Dictionary<NullableObject<T>, IList<T>> _childrenLookup = new();
|
||||||
|
private readonly Dictionary<T, T?> _parentLookup = new();
|
||||||
|
|
||||||
|
private readonly ConcurrentQueue<Action> _hierarchyActions = new();
|
||||||
|
|
||||||
|
public Hierarchy()
|
||||||
|
{
|
||||||
|
_childrenLookup.Add(new NullableObject<T>(), new List<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ProcessChanges()
|
||||||
|
{
|
||||||
|
while (_hierarchyActions.TryDequeue(out var action))
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(T obj)
|
||||||
|
{
|
||||||
|
_hierarchyActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
if (_parentLookup.ContainsKey(obj))
|
||||||
|
throw new ArgumentException("Object is already added to hierarchy");
|
||||||
|
|
||||||
|
_childrenLookup.Add(obj, new List<T>());
|
||||||
|
_parentLookup.Add(obj, null);
|
||||||
|
_childrenLookup[null].Add(obj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(T obj)
|
||||||
|
{
|
||||||
|
foreach (var child in GetChildren(obj))
|
||||||
|
Remove(child);
|
||||||
|
|
||||||
|
_hierarchyActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
var parent = GetParent(obj);
|
||||||
|
_childrenLookup[parent].Remove(obj);
|
||||||
|
|
||||||
|
_parentLookup.Remove(obj);
|
||||||
|
_childrenLookup.Remove(obj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddChild(T parent, T child)
|
||||||
|
{
|
||||||
|
SetParent(child, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetParent(T child, T? parent)
|
||||||
|
{
|
||||||
|
if (child.Equals(parent))
|
||||||
|
throw new InvalidOperationException("Child cannot be parent");
|
||||||
|
|
||||||
|
_hierarchyActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
if (IsInHierarchy(child, parent))
|
||||||
|
throw new InvalidOperationException("Parent is a child of child");
|
||||||
|
|
||||||
|
var oldParent = GetParent(child);
|
||||||
|
_childrenLookup[oldParent].Remove(child);
|
||||||
|
|
||||||
|
_childrenLookup[parent].Add(child);
|
||||||
|
_parentLookup[child] = parent;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(T obj)
|
||||||
|
{
|
||||||
|
return _parentLookup.ContainsKey(obj) && _childrenLookup.ContainsKey(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? GetParent(T child)
|
||||||
|
{
|
||||||
|
return _parentLookup.TryGetValue(child, out var parent)
|
||||||
|
? parent
|
||||||
|
: throw new InvalidOperationException($"Child {child} is not in hierarchy");
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<T> GetChildren(T? obj = null)
|
||||||
|
{
|
||||||
|
return _childrenLookup.TryGetValue(obj, out var children) ? children : Enumerable.Empty<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsInHierarchy(T? ancestor, T? child)
|
||||||
|
{
|
||||||
|
if (child == null) // if child is null (root), then it is not in hierarchy, as root can not have a parent
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ancestor == null) // if ancestor is null (root), then child is not in hierarchy, as root is not a parent
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ancestor.Equals(child))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var parent = GetParent(child);
|
||||||
|
|
||||||
|
if (parent == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ancestor.Equals(parent))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return IsInHierarchy(ancestor, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<T> GetAllChildren(T? obj = null)
|
||||||
|
{
|
||||||
|
var children = GetChildren(obj);
|
||||||
|
|
||||||
|
foreach (var child in children)
|
||||||
|
{
|
||||||
|
yield return child;
|
||||||
|
|
||||||
|
foreach (var descendant in GetAllChildren(child))
|
||||||
|
{
|
||||||
|
yield return descendant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
{
|
||||||
|
return _parentLookup.Keys.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
108
Engine/src/Scene/Scene.cs
Normal file
108
Engine/src/Scene/Scene.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
using Engine.Renderer.Camera;
|
||||||
|
using Engine.Scene.Component.BuiltIn;
|
||||||
|
|
||||||
|
namespace Engine.Scene;
|
||||||
|
|
||||||
|
public class Scene
|
||||||
|
{
|
||||||
|
public bool IsPlaying { get; private set; }
|
||||||
|
public ICamera? Camera { get; private set; }
|
||||||
|
|
||||||
|
internal Hierarchy<GameObject> Hierarchy { get; } = [];
|
||||||
|
|
||||||
|
private readonly Queue<Action> _sceneActions = [];
|
||||||
|
|
||||||
|
internal void Enter()
|
||||||
|
{
|
||||||
|
if (IsPlaying)
|
||||||
|
throw new InvalidOperationException("Scene is already playing");
|
||||||
|
|
||||||
|
ProcessChanges();
|
||||||
|
|
||||||
|
Camera = FindFirstComponent<Camera>();
|
||||||
|
|
||||||
|
IsPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? FindFirstComponent<T>() where T : Component.Component
|
||||||
|
{
|
||||||
|
return Hierarchy.Select(gameObject => gameObject.GetComponent<T>()).OfType<T>().FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Update()
|
||||||
|
{
|
||||||
|
if (!IsPlaying)
|
||||||
|
throw new InvalidOperationException("Scene is not playing");
|
||||||
|
|
||||||
|
ProcessChanges();
|
||||||
|
|
||||||
|
foreach (var gameObject in Hierarchy)
|
||||||
|
gameObject.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Render()
|
||||||
|
{
|
||||||
|
if (!IsPlaying)
|
||||||
|
throw new InvalidOperationException("Scene is not playing");
|
||||||
|
|
||||||
|
foreach (var gameObject in Hierarchy)
|
||||||
|
{
|
||||||
|
gameObject.Render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Exit()
|
||||||
|
{
|
||||||
|
if (!IsPlaying)
|
||||||
|
throw new InvalidOperationException("Scene is not playing");
|
||||||
|
|
||||||
|
foreach (var gameObject in Hierarchy)
|
||||||
|
{
|
||||||
|
gameObject.Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
IsPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(GameObject gameObject)
|
||||||
|
{
|
||||||
|
Hierarchy.Add(gameObject);
|
||||||
|
|
||||||
|
_sceneActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
gameObject.Scene = this;
|
||||||
|
|
||||||
|
gameObject.Awake();
|
||||||
|
gameObject.Start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(GameObject gameObject)
|
||||||
|
{
|
||||||
|
Hierarchy.Remove(gameObject);
|
||||||
|
|
||||||
|
_sceneActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
foreach (var child in Hierarchy.GetAllChildren(gameObject))
|
||||||
|
{
|
||||||
|
child.Destroy();
|
||||||
|
child.Scene = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameObject.Destroy();
|
||||||
|
|
||||||
|
gameObject.Scene = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessChanges()
|
||||||
|
{
|
||||||
|
Hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
while (_sceneActions.TryDequeue(out var action))
|
||||||
|
action();
|
||||||
|
|
||||||
|
foreach (var gameObject in Hierarchy)
|
||||||
|
gameObject.ProcessChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
55
Engine/src/Util/NullableObject.cs
Normal file
55
Engine/src/Util/NullableObject.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
namespace Engine.Util;
|
||||||
|
|
||||||
|
public readonly struct NullableObject<T>
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
public bool IsNull => _value == null;
|
||||||
|
public T? Value => _value;
|
||||||
|
|
||||||
|
private readonly T? _value;
|
||||||
|
|
||||||
|
public NullableObject()
|
||||||
|
{
|
||||||
|
_value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NullableObject(T? value)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator T?(NullableObject<T> nullableObject) => nullableObject.Value;
|
||||||
|
public static implicit operator NullableObject<T>(T? value) => new(value);
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return _value?.ToString() ?? "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
return IsNull;
|
||||||
|
|
||||||
|
if (obj is not NullableObject<T> other)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (IsNull && other.IsNull)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return _value!.Equals(other._value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
if (IsNull)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var hashCode = _value!.GetHashCode();
|
||||||
|
|
||||||
|
if (hashCode >= 0)
|
||||||
|
hashCode += 1;
|
||||||
|
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ namespace Engine;
|
|||||||
public class Window : IPresenter<Rgb8>
|
public class Window : IPresenter<Rgb8>
|
||||||
{
|
{
|
||||||
public bool IsExiting => _window.IsExiting;
|
public bool IsExiting => _window.IsExiting;
|
||||||
public int Width => _window.ClientSize.X;
|
public int Width { get; private set; }
|
||||||
public int Height => _window.ClientSize.Y;
|
public int Height { get; private set; }
|
||||||
|
|
||||||
private readonly Engine _engine;
|
private readonly Engine _engine;
|
||||||
private readonly NativeWindow _window;
|
private readonly NativeWindow _window;
|
||||||
@@ -23,8 +23,14 @@ public class Window : IPresenter<Rgb8>
|
|||||||
_window = window;
|
_window = window;
|
||||||
_headless = headless;
|
_headless = headless;
|
||||||
|
|
||||||
|
(Width, Height) = _window.ClientSize;
|
||||||
|
|
||||||
_window.MakeCurrent();
|
_window.MakeCurrent();
|
||||||
_window.Resize += args => GL.Viewport(0, 0, args.Width, args.Height);
|
_window.Resize += args =>
|
||||||
|
{
|
||||||
|
Width = args.Width;
|
||||||
|
Height = args.Height;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update()
|
public void Update()
|
||||||
71
EngineTests/src/Scene/GameObjectTests.cs
Normal file
71
EngineTests/src/Scene/GameObjectTests.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using Engine.Scene;
|
||||||
|
using Engine.Scene.Component.BuiltIn;
|
||||||
|
|
||||||
|
namespace EngineTests.Scene;
|
||||||
|
|
||||||
|
public class GameObjectTests
|
||||||
|
{
|
||||||
|
private GameObject _gameObject;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_gameObject = new GameObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Constructor_ShouldInitializeWithTransformComponent()
|
||||||
|
{
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_gameObject.Transform, Is.Not.Null);
|
||||||
|
Assert.That(_gameObject.HasComponent<Transform>(), Is.True);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddComponent_ShouldThrowIfComponentAlreadyExists()
|
||||||
|
{
|
||||||
|
_gameObject.AddComponent<Transform>();
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddComponent_ShouldAddComponentToGameObject()
|
||||||
|
{
|
||||||
|
_gameObject.AddComponent<OrthographicCamera>();
|
||||||
|
_gameObject.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_gameObject.GetComponent<OrthographicCamera>(), Is.Not.Null);
|
||||||
|
Assert.That(_gameObject.HasComponent<OrthographicCamera>(), Is.True);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddComponent_ShouldAddComponentToGameObjectWithArgs()
|
||||||
|
{
|
||||||
|
_gameObject.AddComponent<PerspectiveCamera>(99, 0.2f, 1001f);
|
||||||
|
_gameObject.ProcessChanges();
|
||||||
|
|
||||||
|
var camera = _gameObject.GetComponent<PerspectiveCamera>()!;
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(camera.FieldOfView, Is.EqualTo(99));
|
||||||
|
Assert.That(camera.NearPlane, Is.EqualTo(0.2f));
|
||||||
|
Assert.That(camera.FarPlane, Is.EqualTo(1001f));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddComponent_ShouldThrowIfComponentBaseTypeAlreadyExists()
|
||||||
|
{
|
||||||
|
_gameObject.AddComponent<OrthographicCamera>();
|
||||||
|
_gameObject.AddComponent<PerspectiveCamera>();
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
|
||||||
|
}
|
||||||
|
}
|
||||||
566
EngineTests/src/Scene/HierarchyTests.cs
Normal file
566
EngineTests/src/Scene/HierarchyTests.cs
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
using Engine.Scene;
|
||||||
|
|
||||||
|
namespace EngineTests.Scene;
|
||||||
|
|
||||||
|
public class HierarchyTests
|
||||||
|
{
|
||||||
|
private Hierarchy<object> _hierarchy;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_hierarchy = new Hierarchy<object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddToRoot()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
_hierarchy.Add(obj);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(1));
|
||||||
|
Assert.That(_hierarchy.GetParent(obj), Is.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddToChild()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(parent, child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(1));
|
||||||
|
Assert.That(_hierarchy.GetParent(parent), Is.Null);
|
||||||
|
Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RemoveChild()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(parent, child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
_hierarchy.Remove(child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(0));
|
||||||
|
Assert.That(_hierarchy.Contains(child), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RemoveParentAndAllChildren()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var child1 = new object();
|
||||||
|
var child2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child1);
|
||||||
|
_hierarchy.Add(child2);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(parent, child1);
|
||||||
|
_hierarchy.AddChild(parent, child2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
_hierarchy.Remove(parent);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(0));
|
||||||
|
Assert.That(_hierarchy.Contains(child1), Is.False);
|
||||||
|
Assert.That(_hierarchy.Contains(child2), Is.False);
|
||||||
|
Assert.That(_hierarchy.Contains(parent), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DetectCycleInHierarchy()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.AddChild(parent, child);
|
||||||
|
_hierarchy.AddChild(child, parent);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => _hierarchy.ProcessChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_PositiveCase()
|
||||||
|
{
|
||||||
|
var ancestor = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(ancestor);
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(ancestor, child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(ancestor, child), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_NegativeCase()
|
||||||
|
{
|
||||||
|
var obj1 = new object();
|
||||||
|
var obj2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(obj1);
|
||||||
|
_hierarchy.Add(obj2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(obj1, obj2), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void MultipleChildrenHandling()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var child1 = new object();
|
||||||
|
var child2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child1);
|
||||||
|
_hierarchy.Add(child2);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(parent, child1);
|
||||||
|
_hierarchy.AddChild(parent, child2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(2));
|
||||||
|
Assert.That(_hierarchy.GetParent(child1), Is.EqualTo(parent));
|
||||||
|
Assert.That(_hierarchy.GetParent(child2), Is.EqualTo(parent));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetChildren_EmptyCase()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
_hierarchy.Add(obj);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.GetChildren(obj).Count(), Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddDuplicateObjectThrows()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(obj);
|
||||||
|
_hierarchy.Add(obj);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() => _hierarchy.ProcessChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddChild_ParentNotInHierarchyThrows()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
_hierarchy.AddChild(parent, child);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => _hierarchy.ProcessChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetParent_ObjectNotInHierarchyThrows()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.Contains(obj), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NestedHierarchyTest()
|
||||||
|
{
|
||||||
|
var grandParent = new object();
|
||||||
|
var parent = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(grandParent);
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(grandParent, parent);
|
||||||
|
_hierarchy.AddChild(parent, child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetParent(parent), Is.EqualTo(grandParent));
|
||||||
|
Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent));
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(grandParent, child), Is.True);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReparentObjectTest()
|
||||||
|
{
|
||||||
|
var parent1 = new object();
|
||||||
|
var parent2 = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent1);
|
||||||
|
_hierarchy.Add(parent2);
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(parent1, child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
// Reparent the child to parent2
|
||||||
|
_hierarchy.AddChild(parent2, child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren(parent1).Count(), Is.EqualTo(0));
|
||||||
|
Assert.That(_hierarchy.GetChildren(parent2).Count(), Is.EqualTo(1));
|
||||||
|
Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RemoveRootObjectsTest()
|
||||||
|
{
|
||||||
|
var obj1 = new object();
|
||||||
|
var obj2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(obj1);
|
||||||
|
_hierarchy.Add(obj2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
_hierarchy.Remove(obj1);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(1));
|
||||||
|
Assert.That(_hierarchy.Contains(obj1), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_SelfTest()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
_hierarchy.Add(obj);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(obj, obj), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ComplexHierarchyRemovalTest()
|
||||||
|
{
|
||||||
|
var grandParent = new object();
|
||||||
|
var parent1 = new object();
|
||||||
|
var parent2 = new object();
|
||||||
|
var child1 = new object();
|
||||||
|
var child2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(grandParent);
|
||||||
|
_hierarchy.Add(parent1);
|
||||||
|
_hierarchy.Add(parent2);
|
||||||
|
_hierarchy.Add(child1);
|
||||||
|
_hierarchy.Add(child2);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(grandParent, parent1);
|
||||||
|
_hierarchy.AddChild(grandParent, parent2);
|
||||||
|
_hierarchy.AddChild(parent1, child1);
|
||||||
|
_hierarchy.AddChild(parent2, child2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
_hierarchy.Remove(parent1);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren(grandParent).Count(), Is.EqualTo(1));
|
||||||
|
Assert.That(_hierarchy.Contains(child1), Is.False);
|
||||||
|
Assert.That(_hierarchy.Contains(parent1), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddChild_SelfAsParentThrowsException()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
_hierarchy.Add(obj);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => _hierarchy.AddChild(obj, obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void MultiLevelReparentingTest()
|
||||||
|
{
|
||||||
|
var grandParent = new object();
|
||||||
|
var parent = new object();
|
||||||
|
var child1 = new object();
|
||||||
|
var child2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(grandParent);
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child1);
|
||||||
|
_hierarchy.Add(child2);
|
||||||
|
|
||||||
|
// Initial hierarchy setup
|
||||||
|
_hierarchy.AddChild(grandParent, parent);
|
||||||
|
_hierarchy.AddChild(parent, child1);
|
||||||
|
_hierarchy.AddChild(parent, child2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
// Reparent child1 to child2
|
||||||
|
_hierarchy.AddChild(child2, child1);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(1));
|
||||||
|
Assert.That(_hierarchy.GetParent(child1), Is.EqualTo(child2));
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(grandParent, child1), Is.True);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeepHierarchyTest()
|
||||||
|
{
|
||||||
|
const int hierarchyDepth = 100;
|
||||||
|
var objects = new object[hierarchyDepth];
|
||||||
|
|
||||||
|
// Create objects
|
||||||
|
for (var i = 0; i < hierarchyDepth; i++)
|
||||||
|
{
|
||||||
|
objects[i] = new object();
|
||||||
|
_hierarchy.Add(objects[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a deep hierarchy
|
||||||
|
for (var i = 1; i < hierarchyDepth; i++)
|
||||||
|
{
|
||||||
|
_hierarchy.AddChild(objects[i - 1], objects[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
// Verify parent-child relationships
|
||||||
|
for (var i = 1; i < hierarchyDepth; i++)
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetParent(objects[i]), Is.EqualTo(objects[i - 1]));
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(objects[0], objects[i]), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify children
|
||||||
|
for (var i = 0; i < hierarchyDepth - 1; i++)
|
||||||
|
{
|
||||||
|
var children = _hierarchy.GetChildren(objects[i]).ToList();
|
||||||
|
Assert.That(children, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(children[0], Is.EqualTo(objects[i + 1]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RemoveFromMiddleOfHierarchyTest()
|
||||||
|
{
|
||||||
|
var grandParent = new object();
|
||||||
|
var parent = new object();
|
||||||
|
var child1 = new object();
|
||||||
|
var child2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(grandParent);
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child1);
|
||||||
|
_hierarchy.Add(child2);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(grandParent, parent);
|
||||||
|
_hierarchy.AddChild(parent, child1);
|
||||||
|
_hierarchy.AddChild(parent, child2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
// Remove parent, which will also remove its children
|
||||||
|
_hierarchy.Remove(parent);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.GetChildren(grandParent).Count(), Is.EqualTo(0));
|
||||||
|
Assert.That(_hierarchy.Contains(child1), Is.False);
|
||||||
|
Assert.That(_hierarchy.Contains(child2), Is.False);
|
||||||
|
Assert.That(_hierarchy.Contains(parent), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConcurrentModificationTest()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var children = Enumerable.Range(0, 10)
|
||||||
|
.Select(_ => new object())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
children.ForEach(child => _hierarchy.Add(child));
|
||||||
|
|
||||||
|
// Simulate concurrent child additions
|
||||||
|
children.AsParallel().ForAll(child => { _hierarchy.AddChild(parent, child); });
|
||||||
|
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(children.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_NullInputs()
|
||||||
|
{
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
// Null child
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(new object(), null), Is.False);
|
||||||
|
|
||||||
|
// Null ancestor
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(null, new object()), Is.False);
|
||||||
|
|
||||||
|
// Both null
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(null, null), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_SameObject()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
_hierarchy.Add(obj);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(obj, obj), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_DirectParentChild()
|
||||||
|
{
|
||||||
|
var parent = new object();
|
||||||
|
var child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
_hierarchy.AddChild(parent, child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(parent, child), Is.True);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(child, parent), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_DeepHierarchy()
|
||||||
|
{
|
||||||
|
var grandParent = new object();
|
||||||
|
var parent = new object();
|
||||||
|
var child = new object();
|
||||||
|
var grandChild = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(grandParent);
|
||||||
|
_hierarchy.Add(parent);
|
||||||
|
_hierarchy.Add(child);
|
||||||
|
_hierarchy.Add(grandChild);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(grandParent, parent);
|
||||||
|
_hierarchy.AddChild(parent, child);
|
||||||
|
_hierarchy.AddChild(child, grandChild);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
// Verify deep hierarchy relationships
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(grandParent, grandChild), Is.True);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(parent, grandChild), Is.True);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(child, grandChild), Is.True);
|
||||||
|
|
||||||
|
// Verify inverse relationships are false
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(grandChild, grandParent), Is.False);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(grandChild, parent), Is.False);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(grandChild, child), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_SeparateBranches()
|
||||||
|
{
|
||||||
|
var root = new object();
|
||||||
|
var branch1Parent = new object();
|
||||||
|
var branch1Child = new object();
|
||||||
|
var branch2Parent = new object();
|
||||||
|
var branch2Child = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(root);
|
||||||
|
_hierarchy.Add(branch1Parent);
|
||||||
|
_hierarchy.Add(branch1Child);
|
||||||
|
_hierarchy.Add(branch2Parent);
|
||||||
|
_hierarchy.Add(branch2Child);
|
||||||
|
|
||||||
|
_hierarchy.AddChild(root, branch1Parent);
|
||||||
|
_hierarchy.AddChild(root, branch2Parent);
|
||||||
|
_hierarchy.AddChild(branch1Parent, branch1Child);
|
||||||
|
_hierarchy.AddChild(branch2Parent, branch2Child);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
// Verify within same branch
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(root, branch1Parent), Is.True);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(root, branch1Child), Is.True);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch1Child), Is.True);
|
||||||
|
|
||||||
|
// Verify between different branches
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch2Child), Is.False);
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(branch2Parent, branch1Child), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsInHierarchy_ObjectsNotInHierarchy()
|
||||||
|
{
|
||||||
|
var obj1 = new object();
|
||||||
|
var obj2 = new object();
|
||||||
|
|
||||||
|
_hierarchy.Add(obj1);
|
||||||
|
_hierarchy.Add(obj2);
|
||||||
|
_hierarchy.ProcessChanges();
|
||||||
|
|
||||||
|
Assert.That(_hierarchy.IsInHierarchy(obj1, obj2), Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user