This commit is contained in:
2024-12-05 03:19:18 +03:00
parent 3f1740f41f
commit bd156ad028
48 changed files with 1314 additions and 176 deletions

View File

@@ -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

View File

@@ -7,14 +7,16 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="OpenTK" Version="4.8.2" /> <InternalsVisibleTo Include="EngineTests" />
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageReference Include="OpenTK" Version="4.8.2"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog" Version="4.1.0"/>
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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();
} }
} }
} }

View File

@@ -15,7 +15,7 @@ public class IndexBuffer : OpenGlObject
Handle = handle; Handle = handle;
GL.NamedBufferStorage(Handle, Count * sizeof(uint), IntPtr.Zero, flags); GL.NamedBufferStorage(Handle, Count * sizeof(uint), IntPtr.Zero, flags);
Log.Debug("Index buffer {Handle} created with {Count} elements", Handle, Count); Log.Debug("Index buffer {Handle} created with {Count} elements", Handle, Count);
} }

View File

@@ -10,7 +10,7 @@ public class VertexArray : OpenGlObject
{ {
// private IndexBuffer? _boundIndexBuffer; // private IndexBuffer? _boundIndexBuffer;
// private readonly Dictionary<int, VertexBuffer<IVertex>> _boundVertexBuffers = new(); // private readonly Dictionary<int, VertexBuffer<IVertex>> _boundVertexBuffers = new();
public VertexArray() public VertexArray()
{ {
GL.CreateVertexArrays(1, out int handle); GL.CreateVertexArrays(1, out int handle);
@@ -20,7 +20,7 @@ public class VertexArray : OpenGlObject
public void BindIndexBuffer(IndexBuffer buffer) public void BindIndexBuffer(IndexBuffer buffer)
{ {
GL.VertexArrayElementBuffer(Handle, buffer.Handle); GL.VertexArrayElementBuffer(Handle, buffer.Handle);
Log.Debug("Vertex array {Handle} bound to index buffer {Buffer}", Handle, buffer.Handle); Log.Debug("Vertex array {Handle} bound to index buffer {Buffer}", Handle, buffer.Handle);
} }

View File

@@ -26,7 +26,7 @@ public class VertexBuffer<T> : OpenGlObject
Handle = handle; Handle = handle;
GL.NamedBufferStorage(Handle, Count * _stride, IntPtr.Zero, flags); 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); Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name);
} }

View File

@@ -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; }
} }

View File

@@ -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; }
} }

View File

@@ -1,38 +1,18 @@
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,
IntPtr message, IntPtr userParam) IntPtr message, IntPtr userParam)
{ {
@@ -53,7 +33,7 @@ public class Renderer
DebugType.DebugTypeOther => "Info", DebugType.DebugTypeOther => "Info",
_ => "Unknown" _ => "Unknown"
}; };
var sourcePrefix = source switch var sourcePrefix = source switch
{ {
DebugSource.DebugSourceApi => "API", DebugSource.DebugSourceApi => "API",
@@ -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);
}
} }

View File

@@ -29,7 +29,7 @@ public class Framebuffer : OpenGlObject
_height = value; _height = value;
} }
} }
public IConstTexture<Rgb8> Texture => _texture; public IConstTexture<Rgb8> Texture => _texture;
internal Texture.Texture<Rgb8> TextureInternal => _texture; internal Texture.Texture<Rgb8> TextureInternal => _texture;

View File

@@ -15,7 +15,7 @@ public abstract class OpenGlObject
{ {
Destroy(); Destroy();
Handle = -1; Handle = -1;
Log.Debug("OpenGL object {Handle} destroyed", Handle); Log.Debug("OpenGL object {Handle} destroyed", Handle);
} }
} }

View 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);
}
}

View File

@@ -42,7 +42,7 @@ public abstract class Texture<T> : OpenGlObject, ITexture<T> where T : struct, I
GL.CreateTextures(TextureTarget.Texture2D, 1, out int handle); GL.CreateTextures(TextureTarget.Texture2D, 1, out int handle);
Handle = handle; Handle = handle;
} }
public void UploadPixels(int x, int y, int width, int height, T[,] pixels) public void UploadPixels(int x, int y, int width, int height, T[,] pixels)
{ {
if (x < 0 || y < 0) if (x < 0 || y < 0)

View 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;
}
}
}

View 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,
}
}

View File

@@ -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);
} }

View File

@@ -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;
}
} }

View 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;
}
}

View 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();
}
}

View 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
View 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();
}
}

View 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;
}
}

View File

@@ -10,9 +10,9 @@ 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;
private readonly bool _headless; private readonly bool _headless;
@@ -22,9 +22,15 @@ public class Window : IPresenter<Rgb8>
_engine = engine; _engine = engine;
_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()
@@ -42,30 +48,30 @@ public class Window : IPresenter<Rgb8>
return; return;
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_engine.Renderer.TextureInternal.Bind(); _engine.Renderer.TextureInternal.Bind();
GL.Enable(EnableCap.Texture2D); GL.Enable(EnableCap.Texture2D);
GL.Begin(PrimitiveType.Quads); GL.Begin(PrimitiveType.Quads);
GL.Color3(1, 1, 1); GL.Color3(1, 1, 1);
GL.TexCoord2(0, 0); GL.TexCoord2(0, 0);
GL.Vertex2(0, 0); GL.Vertex2(0, 0);
GL.TexCoord2(1, 0); GL.TexCoord2(1, 0);
GL.Vertex2(1, 0); GL.Vertex2(1, 0);
GL.TexCoord2(1, 1); GL.TexCoord2(1, 1);
GL.Vertex2(1, 1); GL.Vertex2(1, 1);
GL.TexCoord2(0, 1); GL.TexCoord2(0, 1);
GL.Vertex2(0, 1); GL.Vertex2(0, 1);
GL.End(); GL.End();
GL.Disable(EnableCap.Texture2D); GL.Disable(EnableCap.Texture2D);
GL.Flush(); GL.Flush();
_engine.Renderer.TextureInternal.Unbind(); _engine.Renderer.TextureInternal.Unbind();
} }
} }

View 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());
}
}

View 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);
}
}