diff --git a/DoomDeathmatch.sln b/DoomDeathmatch.sln
index 8d9962d..bd717de 100644
--- a/DoomDeathmatch.sln
+++ b/DoomDeathmatch.sln
@@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatchConsole", "Do
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatchWPF", "DoomDeathmatchWPF\DoomDeathmatchWPF.csproj", "{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineTests", "EngineTests\EngineTests.csproj", "{CC28C26C-0998-4C13-8855-978658B8B0D6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
EndGlobal
diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj
index 31d943d..67df8d7 100644
--- a/Engine/Engine.csproj
+++ b/Engine/Engine.csproj
@@ -7,14 +7,16 @@
- true
+ true
-
-
-
-
+
+
+
+
+
+
diff --git a/Engine/Scene/Component/Component.cs b/Engine/Scene/Component/Component.cs
deleted file mode 100644
index 37066e4..0000000
--- a/Engine/Scene/Component/Component.cs
+++ /dev/null
@@ -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;
-}
\ No newline at end of file
diff --git a/Engine/Scene/GameObject.cs b/Engine/Scene/GameObject.cs
deleted file mode 100644
index 61e9c58..0000000
--- a/Engine/Scene/GameObject.cs
+++ /dev/null
@@ -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 _componentActions = new();
-
- private readonly List _components = new();
- private readonly ISet _addedComponentTypes = new HashSet();
-
- public GameObject()
- {
- AddComponent();
- UpdateComponents();
-
- Transform = GetComponent()!;
- }
-
- public T? GetComponent() where T : Component.Component
- {
- if (!_addedComponentTypes.Contains(typeof(T)))
- return null;
-
- return _components.OfType().FirstOrDefault();
- }
-
- public void AddComponent(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() where T : Component.Component
- {
- if (!_addedComponentTypes.Contains(typeof(T)) || typeof(T) == typeof(Transform))
- return;
-
- var component = GetComponent();
- if (component == null)
- return;
-
- _componentActions.Enqueue(() =>
- {
- _components.Remove(component);
- _addedComponentTypes.Remove(typeof(T));
- });
- }
-
- private void UpdateComponents()
- {
- while (_componentActions.TryDequeue(out var action))
- action();
- }
-}
\ No newline at end of file
diff --git a/Engine/Asset/Image.cs b/Engine/src/Asset/Image.cs
similarity index 100%
rename from Engine/Asset/Image.cs
rename to Engine/src/Asset/Image.cs
diff --git a/Engine/Asset/Mesh/Loader/IMeshLoader.cs b/Engine/src/Asset/Mesh/Loader/IMeshLoader.cs
similarity index 100%
rename from Engine/Asset/Mesh/Loader/IMeshLoader.cs
rename to Engine/src/Asset/Mesh/Loader/IMeshLoader.cs
diff --git a/Engine/Asset/Mesh/Loader/MeshLoaderParameters.cs b/Engine/src/Asset/Mesh/Loader/MeshLoaderParameters.cs
similarity index 100%
rename from Engine/Asset/Mesh/Loader/MeshLoaderParameters.cs
rename to Engine/src/Asset/Mesh/Loader/MeshLoaderParameters.cs
diff --git a/Engine/Asset/Mesh/Loader/ObjMeshLoader.cs b/Engine/src/Asset/Mesh/Loader/ObjMeshLoader.cs
similarity index 100%
rename from Engine/Asset/Mesh/Loader/ObjMeshLoader.cs
rename to Engine/src/Asset/Mesh/Loader/ObjMeshLoader.cs
diff --git a/Engine/Asset/Mesh/Loader/StlMeshLoader.cs b/Engine/src/Asset/Mesh/Loader/StlMeshLoader.cs
similarity index 100%
rename from Engine/Asset/Mesh/Loader/StlMeshLoader.cs
rename to Engine/src/Asset/Mesh/Loader/StlMeshLoader.cs
diff --git a/Engine/Asset/Mesh/Mesh.cs b/Engine/src/Asset/Mesh/Mesh.cs
similarity index 100%
rename from Engine/Asset/Mesh/Mesh.cs
rename to Engine/src/Asset/Mesh/Mesh.cs
diff --git a/Engine/Engine.cs b/Engine/src/Engine.cs
similarity index 89%
rename from Engine/Engine.cs
rename to Engine/src/Engine.cs
index 9787f8e..3d6329a 100644
--- a/Engine/Engine.cs
+++ b/Engine/src/Engine.cs
@@ -1,14 +1,8 @@
-using System.Runtime.InteropServices;
-using System.Text;
-using Engine.Renderer.Buffer;
-using Engine.Renderer.Buffer.Vertex;
-using Engine.Renderer.Shader;
-using OpenTK.Graphics.OpenGL;
+using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using Serilog;
-using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
namespace Engine;
@@ -59,7 +53,7 @@ public sealed class Engine
while (!_window.IsExiting)
{
-
+ _window.Update();
}
}
}
\ No newline at end of file
diff --git a/Engine/Input/IInputHandler.cs b/Engine/src/Input/IInputHandler.cs
similarity index 100%
rename from Engine/Input/IInputHandler.cs
rename to Engine/src/Input/IInputHandler.cs
diff --git a/Engine/Input/Key.cs b/Engine/src/Input/Key.cs
similarity index 100%
rename from Engine/Input/Key.cs
rename to Engine/src/Input/Key.cs
diff --git a/Engine/Input/MouseButton.cs b/Engine/src/Input/MouseButton.cs
similarity index 100%
rename from Engine/Input/MouseButton.cs
rename to Engine/src/Input/MouseButton.cs
diff --git a/Engine/Renderer/Buffer/IndexBuffer.cs b/Engine/src/Renderer/Buffer/IndexBuffer.cs
similarity index 99%
rename from Engine/Renderer/Buffer/IndexBuffer.cs
rename to Engine/src/Renderer/Buffer/IndexBuffer.cs
index 497d731..82576ee 100644
--- a/Engine/Renderer/Buffer/IndexBuffer.cs
+++ b/Engine/src/Renderer/Buffer/IndexBuffer.cs
@@ -15,7 +15,7 @@ public class IndexBuffer : OpenGlObject
Handle = handle;
GL.NamedBufferStorage(Handle, Count * sizeof(uint), IntPtr.Zero, flags);
-
+
Log.Debug("Index buffer {Handle} created with {Count} elements", Handle, Count);
}
diff --git a/Engine/Renderer/Buffer/Vertex/IVertex.cs b/Engine/src/Renderer/Buffer/Vertex/IVertex.cs
similarity index 100%
rename from Engine/Renderer/Buffer/Vertex/IVertex.cs
rename to Engine/src/Renderer/Buffer/Vertex/IVertex.cs
diff --git a/Engine/Renderer/Buffer/Vertex/VertexAttribute.cs b/Engine/src/Renderer/Buffer/Vertex/VertexAttribute.cs
similarity index 100%
rename from Engine/Renderer/Buffer/Vertex/VertexAttribute.cs
rename to Engine/src/Renderer/Buffer/Vertex/VertexAttribute.cs
diff --git a/Engine/Renderer/Buffer/VertexArray.cs b/Engine/src/Renderer/Buffer/VertexArray.cs
similarity index 99%
rename from Engine/Renderer/Buffer/VertexArray.cs
rename to Engine/src/Renderer/Buffer/VertexArray.cs
index 3f7f7c2..10c1454 100644
--- a/Engine/Renderer/Buffer/VertexArray.cs
+++ b/Engine/src/Renderer/Buffer/VertexArray.cs
@@ -10,7 +10,7 @@ public class VertexArray : OpenGlObject
{
// private IndexBuffer? _boundIndexBuffer;
// private readonly Dictionary> _boundVertexBuffers = new();
-
+
public VertexArray()
{
GL.CreateVertexArrays(1, out int handle);
@@ -20,7 +20,7 @@ public class VertexArray : OpenGlObject
public void BindIndexBuffer(IndexBuffer buffer)
{
GL.VertexArrayElementBuffer(Handle, buffer.Handle);
-
+
Log.Debug("Vertex array {Handle} bound to index buffer {Buffer}", Handle, buffer.Handle);
}
diff --git a/Engine/Renderer/Buffer/VertexBuffer.cs b/Engine/src/Renderer/Buffer/VertexBuffer.cs
similarity index 99%
rename from Engine/Renderer/Buffer/VertexBuffer.cs
rename to Engine/src/Renderer/Buffer/VertexBuffer.cs
index aea1fd1..8fe1693 100644
--- a/Engine/Renderer/Buffer/VertexBuffer.cs
+++ b/Engine/src/Renderer/Buffer/VertexBuffer.cs
@@ -26,7 +26,7 @@ public class VertexBuffer : OpenGlObject
Handle = handle;
GL.NamedBufferStorage(Handle, Count * _stride, IntPtr.Zero, flags);
-
+
Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name);
}
diff --git a/Engine/Renderer/Camera/ICamera.cs b/Engine/src/Renderer/Camera/ICamera.cs
similarity index 75%
rename from Engine/Renderer/Camera/ICamera.cs
rename to Engine/src/Renderer/Camera/ICamera.cs
index af339af..9c385bc 100644
--- a/Engine/Renderer/Camera/ICamera.cs
+++ b/Engine/src/Renderer/Camera/ICamera.cs
@@ -6,4 +6,5 @@ public interface ICamera
{
public Matrix4 View { get; }
public Matrix4 Projection { get; }
+ public Vector2i ScreenSize { get; internal set; }
}
\ No newline at end of file
diff --git a/Engine/Renderer/Camera/ScreenspaceCamera.cs b/Engine/src/Renderer/Camera/ScreenspaceCamera.cs
similarity index 82%
rename from Engine/Renderer/Camera/ScreenspaceCamera.cs
rename to Engine/src/Renderer/Camera/ScreenspaceCamera.cs
index 0e5db51..41d2dee 100644
--- a/Engine/Renderer/Camera/ScreenspaceCamera.cs
+++ b/Engine/src/Renderer/Camera/ScreenspaceCamera.cs
@@ -6,4 +6,5 @@ public class ScreenspaceCamera : ICamera
{
public Matrix4 View => Matrix4.Identity;
public Matrix4 Projection => Matrix4.Identity;
+ public Vector2i ScreenSize { get; set; }
}
\ No newline at end of file
diff --git a/Engine/Renderer/Renderer.cs b/Engine/src/Renderer/Debug.cs
similarity index 66%
rename from Engine/Renderer/Renderer.cs
rename to Engine/src/Renderer/Debug.cs
index b1ed6d9..1eb2e65 100644
--- a/Engine/Renderer/Renderer.cs
+++ b/Engine/src/Renderer/Debug.cs
@@ -1,38 +1,18 @@
using System.Runtime.InteropServices;
-using Engine.Renderer.Pixel;
using OpenTK.Graphics.OpenGL;
using Serilog;
using Serilog.Events;
namespace Engine.Renderer;
-public class Renderer
+internal static class Debug
{
- internal Texture.Texture TextureInternal => _framebuffer.TextureInternal;
-
- private readonly Framebuffer.Framebuffer _framebuffer;
- private readonly Queue> _renderActions = new();
-
- public Renderer(int width, int height)
- {
- InitializeOpenGl();
-
- _framebuffer = new Framebuffer.Framebuffer(width, height);
- }
-
- private void InitializeOpenGl()
+ public static void Setup()
{
GL.Enable(EnableCap.DebugOutput);
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,
IntPtr message, IntPtr userParam)
{
@@ -53,7 +33,7 @@ public class Renderer
DebugType.DebugTypeOther => "Info",
_ => "Unknown"
};
-
+
var sourcePrefix = source switch
{
DebugSource.DebugSourceApi => "API",
@@ -65,8 +45,7 @@ public class Renderer
_ => "Unknown"
};
- logger.Write(
- GetLogLevel(severity),
+ logger.Write(GetLogLevel(severity),
"[OpenGL {TypePrefix}] [{Source}] {Message} (ID: 0x{Id:X8})",
typePrefix,
sourcePrefix,
@@ -86,22 +65,4 @@ public class Renderer
_ => LogEventLevel.Debug
};
}
-
- internal void Commit(Action 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);
- }
}
\ No newline at end of file
diff --git a/Engine/Renderer/Framebuffer/Framebuffer.cs b/Engine/src/Renderer/Framebuffer/Framebuffer.cs
similarity index 99%
rename from Engine/Renderer/Framebuffer/Framebuffer.cs
rename to Engine/src/Renderer/Framebuffer/Framebuffer.cs
index fe32ec4..8c49660 100644
--- a/Engine/Renderer/Framebuffer/Framebuffer.cs
+++ b/Engine/src/Renderer/Framebuffer/Framebuffer.cs
@@ -29,7 +29,7 @@ public class Framebuffer : OpenGlObject
_height = value;
}
}
-
+
public IConstTexture Texture => _texture;
internal Texture.Texture TextureInternal => _texture;
diff --git a/Engine/Renderer/Framebuffer/Renderbuffer.cs b/Engine/src/Renderer/Framebuffer/Renderbuffer.cs
similarity index 100%
rename from Engine/Renderer/Framebuffer/Renderbuffer.cs
rename to Engine/src/Renderer/Framebuffer/Renderbuffer.cs
diff --git a/Engine/Renderer/IPresenter.cs b/Engine/src/Renderer/IPresenter.cs
similarity index 100%
rename from Engine/Renderer/IPresenter.cs
rename to Engine/src/Renderer/IPresenter.cs
diff --git a/Engine/Renderer/OpenGLObject.cs b/Engine/src/Renderer/OpenGLObject.cs
similarity index 97%
rename from Engine/Renderer/OpenGLObject.cs
rename to Engine/src/Renderer/OpenGLObject.cs
index 5069989..57a1e65 100644
--- a/Engine/Renderer/OpenGLObject.cs
+++ b/Engine/src/Renderer/OpenGLObject.cs
@@ -15,7 +15,7 @@ public abstract class OpenGlObject
{
Destroy();
Handle = -1;
-
+
Log.Debug("OpenGL object {Handle} destroyed", Handle);
}
}
\ No newline at end of file
diff --git a/Engine/Renderer/Pixel/IPixel.cs b/Engine/src/Renderer/Pixel/IPixel.cs
similarity index 100%
rename from Engine/Renderer/Pixel/IPixel.cs
rename to Engine/src/Renderer/Pixel/IPixel.cs
diff --git a/Engine/Renderer/Pixel/Rgb8.cs b/Engine/src/Renderer/Pixel/Rgb8.cs
similarity index 100%
rename from Engine/Renderer/Pixel/Rgb8.cs
rename to Engine/src/Renderer/Pixel/Rgb8.cs
diff --git a/Engine/src/Renderer/Renderer.cs b/Engine/src/Renderer/Renderer.cs
new file mode 100644
index 0000000..f691cbc
--- /dev/null
+++ b/Engine/src/Renderer/Renderer.cs
@@ -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 TextureInternal => _framebuffer.TextureInternal;
+
+ private readonly Framebuffer.Framebuffer _framebuffer;
+ private readonly Queue> _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 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);
+ }
+}
\ No newline at end of file
diff --git a/Engine/Renderer/Shader/ShaderProgram.cs b/Engine/src/Renderer/Shader/ShaderProgram.cs
similarity index 100%
rename from Engine/Renderer/Shader/ShaderProgram.cs
rename to Engine/src/Renderer/Shader/ShaderProgram.cs
diff --git a/Engine/Renderer/Texture/DynamicTexture.cs b/Engine/src/Renderer/Texture/DynamicTexture.cs
similarity index 100%
rename from Engine/Renderer/Texture/DynamicTexture.cs
rename to Engine/src/Renderer/Texture/DynamicTexture.cs
diff --git a/Engine/Renderer/Texture/IConstTexture.cs b/Engine/src/Renderer/Texture/IConstTexture.cs
similarity index 100%
rename from Engine/Renderer/Texture/IConstTexture.cs
rename to Engine/src/Renderer/Texture/IConstTexture.cs
diff --git a/Engine/Renderer/Texture/ITexture.cs b/Engine/src/Renderer/Texture/ITexture.cs
similarity index 100%
rename from Engine/Renderer/Texture/ITexture.cs
rename to Engine/src/Renderer/Texture/ITexture.cs
diff --git a/Engine/Renderer/Texture/StaticTexture.cs b/Engine/src/Renderer/Texture/StaticTexture.cs
similarity index 100%
rename from Engine/Renderer/Texture/StaticTexture.cs
rename to Engine/src/Renderer/Texture/StaticTexture.cs
diff --git a/Engine/Renderer/Texture/Texture.cs b/Engine/src/Renderer/Texture/Texture.cs
similarity index 99%
rename from Engine/Renderer/Texture/Texture.cs
rename to Engine/src/Renderer/Texture/Texture.cs
index f5a6e97..ec435f4 100644
--- a/Engine/Renderer/Texture/Texture.cs
+++ b/Engine/src/Renderer/Texture/Texture.cs
@@ -42,7 +42,7 @@ public abstract class Texture : OpenGlObject, ITexture where T : struct, I
GL.CreateTextures(TextureTarget.Texture2D, 1, out int handle);
Handle = handle;
}
-
+
public void UploadPixels(int x, int y, int width, int height, T[,] pixels)
{
if (x < 0 || y < 0)
diff --git a/Engine/src/Scene/Component/BuiltIn/Camera.cs b/Engine/src/Scene/Component/BuiltIn/Camera.cs
new file mode 100644
index 0000000..583193f
--- /dev/null
+++ b/Engine/src/Scene/Component/BuiltIn/Camera.cs
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Engine/src/Scene/Component/BuiltIn/OrthographicCamera.cs b/Engine/src/Scene/Component/BuiltIn/OrthographicCamera.cs
new file mode 100644
index 0000000..6f0f3c7
--- /dev/null
+++ b/Engine/src/Scene/Component/BuiltIn/OrthographicCamera.cs
@@ -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,
+ }
+}
\ No newline at end of file
diff --git a/Engine/Scene/Component/PerspectiveCamera.cs b/Engine/src/Scene/Component/BuiltIn/PerspectiveCamera.cs
similarity index 50%
rename from Engine/Scene/Component/PerspectiveCamera.cs
rename to Engine/src/Scene/Component/BuiltIn/PerspectiveCamera.cs
index 7b58b2e..44ff400 100644
--- a/Engine/Scene/Component/PerspectiveCamera.cs
+++ b/Engine/src/Scene/Component/BuiltIn/PerspectiveCamera.cs
@@ -1,23 +1,18 @@
-using Engine.Renderer.Camera;
-using Engine.Util;
+using Engine.Util;
using OpenTK.Mathematics;
-namespace Engine.Scene.Component;
+namespace Engine.Scene.Component.BuiltIn;
public class PerspectiveCamera(
- GameObject gameObject,
- float aspectRatio,
- float fieldOfView,
- float nearPlane,
- float farPlane)
- : Component(gameObject), ICamera
+ float fieldOfView = 90.0f,
+ float nearPlane = 0.1f,
+ float farPlane = 1000f
+)
+ : Camera(nearPlane, farPlane)
{
- public float AspectRatio { get; set; } = aspectRatio;
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
{
@@ -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);
}
\ No newline at end of file
diff --git a/Engine/Scene/Component/Transform.cs b/Engine/src/Scene/Component/BuiltIn/Transform.cs
similarity index 50%
rename from Engine/Scene/Component/Transform.cs
rename to Engine/src/Scene/Component/BuiltIn/Transform.cs
index cfa80a9..2e48368 100644
--- a/Engine/Scene/Component/Transform.cs
+++ b/Engine/src/Scene/Component/BuiltIn/Transform.cs
@@ -1,8 +1,8 @@
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 Quaternion Rotation { get; set; } = Quaternion.Identity;
@@ -15,5 +15,25 @@ public class Transform(GameObject gameObject) : Component(gameObject)
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;
+ }
}
\ No newline at end of file
diff --git a/Engine/src/Scene/Component/Component.cs b/Engine/src/Scene/Component/Component.cs
new file mode 100644
index 0000000..c6d3e32
--- /dev/null
+++ b/Engine/src/Scene/Component/Component.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/Engine/src/Scene/GameObject.cs b/Engine/src/Scene/GameObject.cs
new file mode 100644
index 0000000..2c3540c
--- /dev/null
+++ b/Engine/src/Scene/GameObject.cs
@@ -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 _componentActions = new();
+
+ private readonly IList _components = new List();
+ private readonly ISet _addedComponentTypes = new HashSet();
+
+ public GameObject()
+ {
+ AddComponent();
+ ProcessChanges();
+
+ Transform = GetComponent()!;
+ }
+
+ public GameObject(Transform transform)
+ {
+ AddComponent(transform.Clone());
+ ProcessChanges();
+
+ Transform = GetComponent()!;
+ }
+
+ 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() where T : Component.Component
+ {
+ return !HasComponent() ? null : _components.OfType().First();
+ }
+
+ public void AddComponent() where T : Component.Component, new()
+ {
+ var component = new T();
+ AddComponent(component);
+ }
+
+ public void AddComponent(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 component) where T : Component.Component
+ {
+ _componentActions.Enqueue(() =>
+ {
+ if (HasComponent())
+ 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() where T : Component.Component
+ {
+ if (typeof(T) == typeof(Transform))
+ throw new ArgumentException("GameObject cannot remove Transform component");
+
+ _componentActions.Enqueue(() =>
+ {
+ if (!HasComponent())
+ throw new ArgumentException($"GameObject does not have component of type {typeof(T)}");
+
+ var component = GetComponent();
+ if (component == null)
+ return;
+
+ _components.Remove(component);
+ _addedComponentTypes.Remove(typeof(T));
+ });
+ }
+
+ public bool HasComponent() where T : Component.Component
+ {
+ var baseType = typeof(T).GetComponentBaseType();
+ return _addedComponentTypes.Contains(baseType);
+ }
+
+ internal void ProcessChanges()
+ {
+ while (_componentActions.TryDequeue(out var action))
+ action();
+ }
+}
\ No newline at end of file
diff --git a/Engine/src/Scene/Hierarchy.cs b/Engine/src/Scene/Hierarchy.cs
new file mode 100644
index 0000000..643babb
--- /dev/null
+++ b/Engine/src/Scene/Hierarchy.cs
@@ -0,0 +1,140 @@
+using System.Collections;
+using System.Collections.Concurrent;
+using Engine.Util;
+
+namespace Engine.Scene;
+
+public class Hierarchy : IEnumerable
+ where T : class
+{
+ private readonly Dictionary, IList> _childrenLookup = new();
+ private readonly Dictionary _parentLookup = new();
+
+ private readonly ConcurrentQueue _hierarchyActions = new();
+
+ public Hierarchy()
+ {
+ _childrenLookup.Add(new NullableObject(), new List());
+ }
+
+ 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());
+ _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 GetChildren(T? obj = null)
+ {
+ return _childrenLookup.TryGetValue(obj, out var children) ? children : Enumerable.Empty();
+ }
+
+ 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 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 GetEnumerator()
+ {
+ return _parentLookup.Keys.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+}
\ No newline at end of file
diff --git a/Engine/src/Scene/Scene.cs b/Engine/src/Scene/Scene.cs
new file mode 100644
index 0000000..001e78b
--- /dev/null
+++ b/Engine/src/Scene/Scene.cs
@@ -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 Hierarchy { get; } = [];
+
+ private readonly Queue _sceneActions = [];
+
+ internal void Enter()
+ {
+ if (IsPlaying)
+ throw new InvalidOperationException("Scene is already playing");
+
+ ProcessChanges();
+
+ Camera = FindFirstComponent();
+
+ IsPlaying = true;
+ }
+
+ public T? FindFirstComponent() where T : Component.Component
+ {
+ return Hierarchy.Select(gameObject => gameObject.GetComponent()).OfType().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();
+ }
+}
\ No newline at end of file
diff --git a/Engine/Util/Math.cs b/Engine/src/Util/Math.cs
similarity index 100%
rename from Engine/Util/Math.cs
rename to Engine/src/Util/Math.cs
diff --git a/Engine/src/Util/NullableObject.cs b/Engine/src/Util/NullableObject.cs
new file mode 100644
index 0000000..25c7e24
--- /dev/null
+++ b/Engine/src/Util/NullableObject.cs
@@ -0,0 +1,55 @@
+namespace Engine.Util;
+
+public readonly struct NullableObject
+ 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 nullableObject) => nullableObject.Value;
+ public static implicit operator NullableObject(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 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;
+ }
+}
\ No newline at end of file
diff --git a/Engine/Window.cs b/Engine/src/Window.cs
similarity index 86%
rename from Engine/Window.cs
rename to Engine/src/Window.cs
index 28d39bf..16adaca 100644
--- a/Engine/Window.cs
+++ b/Engine/src/Window.cs
@@ -10,9 +10,9 @@ namespace Engine;
public class Window : IPresenter
{
public bool IsExiting => _window.IsExiting;
- public int Width => _window.ClientSize.X;
- public int Height => _window.ClientSize.Y;
-
+ public int Width { get; private set; }
+ public int Height { get; private set; }
+
private readonly Engine _engine;
private readonly NativeWindow _window;
private readonly bool _headless;
@@ -22,9 +22,15 @@ public class Window : IPresenter
_engine = engine;
_window = window;
_headless = headless;
-
+
+ (Width, Height) = _window.ClientSize;
+
_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()
@@ -42,30 +48,30 @@ public class Window : IPresenter
return;
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
-
+
_engine.Renderer.TextureInternal.Bind();
-
+
GL.Enable(EnableCap.Texture2D);
GL.Begin(PrimitiveType.Quads);
GL.Color3(1, 1, 1);
-
+
GL.TexCoord2(0, 0);
GL.Vertex2(0, 0);
-
+
GL.TexCoord2(1, 0);
GL.Vertex2(1, 0);
-
+
GL.TexCoord2(1, 1);
GL.Vertex2(1, 1);
-
+
GL.TexCoord2(0, 1);
GL.Vertex2(0, 1);
-
+
GL.End();
-
+
GL.Disable(EnableCap.Texture2D);
GL.Flush();
-
+
_engine.Renderer.TextureInternal.Unbind();
}
}
diff --git a/EngineTests/src/Scene/GameObjectTests.cs b/EngineTests/src/Scene/GameObjectTests.cs
new file mode 100644
index 0000000..c559edf
--- /dev/null
+++ b/EngineTests/src/Scene/GameObjectTests.cs
@@ -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(), Is.True);
+ });
+ }
+
+ [Test]
+ public void AddComponent_ShouldThrowIfComponentAlreadyExists()
+ {
+ _gameObject.AddComponent();
+
+ Assert.Throws(() => _gameObject.ProcessChanges());
+ }
+
+ [Test]
+ public void AddComponent_ShouldAddComponentToGameObject()
+ {
+ _gameObject.AddComponent();
+ _gameObject.ProcessChanges();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(_gameObject.GetComponent(), Is.Not.Null);
+ Assert.That(_gameObject.HasComponent(), Is.True);
+ });
+ }
+
+ [Test]
+ public void AddComponent_ShouldAddComponentToGameObjectWithArgs()
+ {
+ _gameObject.AddComponent(99, 0.2f, 1001f);
+ _gameObject.ProcessChanges();
+
+ var camera = _gameObject.GetComponent()!;
+
+ 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();
+ _gameObject.AddComponent();
+
+ Assert.Throws(() => _gameObject.ProcessChanges());
+ }
+}
\ No newline at end of file
diff --git a/EngineTests/src/Scene/HierarchyTests.cs b/EngineTests/src/Scene/HierarchyTests.cs
new file mode 100644
index 0000000..2b9e426
--- /dev/null
+++ b/EngineTests/src/Scene/HierarchyTests.cs
@@ -0,0 +1,566 @@
+using Engine.Scene;
+
+namespace EngineTests.Scene;
+
+public class HierarchyTests
+{
+ private Hierarchy