diff --git a/Engine b/Engine
index ad444de..b9f3227 160000
--- a/Engine
+++ b/Engine
@@ -1 +1 @@
-Subproject commit ad444decbb7fad331278415d2437c9a01df694ab
+Subproject commit b9f3227f737a61be2860b998ca9ed99e268dcbf5
diff --git a/Platforms/Desktop/Desktop.csproj b/Platforms/Desktop/Desktop.csproj
index e41705f..6d52c57 100644
--- a/Platforms/Desktop/Desktop.csproj
+++ b/Platforms/Desktop/Desktop.csproj
@@ -10,6 +10,7 @@
MyUniverse
+ true
app.manifest
Icon.ico
@@ -28,6 +29,7 @@
+
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/CameraController2D.cs b/Platforms/Desktop/Engine.Integration.SDL3/CameraController2D.cs
new file mode 100644
index 0000000..039eb52
--- /dev/null
+++ b/Platforms/Desktop/Engine.Integration.SDL3/CameraController2D.cs
@@ -0,0 +1,31 @@
+using Engine.Core;
+
+using SDL3;
+
+namespace Engine.Integration.SDL3;
+
+public class CameraController2D : Behaviour2D, IFirstFrameUpdate, IUpdate
+{
+ private Sdl3KeyboardInputs keyboardInputs = null!;
+ private ICamera2D camera = null!;
+
+ public void FirstActiveFrame()
+ {
+ keyboardInputs = Universe.FindRequiredBehaviour();
+ camera = BehaviourController.GetRequiredBehaviourInParent();
+ }
+
+ public void Update()
+ {
+ if (keyboardInputs.IsPressed(SDL.Keycode.W)) Transform.Position += Transform.Up * Universe.Time.DeltaTime * 100f;
+ if (keyboardInputs.IsPressed(SDL.Keycode.A)) Transform.Position += Transform.Left * Universe.Time.DeltaTime * 100f;
+ if (keyboardInputs.IsPressed(SDL.Keycode.S)) Transform.Position += Transform.Down * Universe.Time.DeltaTime * 100f;
+ if (keyboardInputs.IsPressed(SDL.Keycode.D)) Transform.Position += Transform.Right * Universe.Time.DeltaTime * 100f;
+
+ if (keyboardInputs.IsPressed(SDL.Keycode.Q)) Transform.Rotation += Universe.Time.DeltaTime * 45f;
+ if (keyboardInputs.IsPressed(SDL.Keycode.E)) Transform.Rotation -= Universe.Time.DeltaTime * 45f;
+
+ if (keyboardInputs.IsPressed(SDL.Keycode.R)) camera.Zoom += Universe.Time.DeltaTime * 15f;
+ if (keyboardInputs.IsPressed(SDL.Keycode.F)) camera.Zoom -= Universe.Time.DeltaTime * 15f;
+ }
+}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/ISdl3EventProcessor.cs b/Platforms/Desktop/Engine.Integration.SDL3/ISdl3EventProcessor.cs
new file mode 100644
index 0000000..578f8bd
--- /dev/null
+++ b/Platforms/Desktop/Engine.Integration.SDL3/ISdl3EventProcessor.cs
@@ -0,0 +1,8 @@
+using SDL3;
+
+namespace Engine.Integration.SDL3;
+
+public interface ISdl3EventProcessor
+{
+ void Process(SDL.Event sdlEvent);
+}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/Sdl3Camera2D.cs b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3Camera2D.cs
new file mode 100644
index 0000000..9750799
--- /dev/null
+++ b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3Camera2D.cs
@@ -0,0 +1,86 @@
+using Engine.Core;
+
+namespace Engine.Integration.SDL3;
+
+public class Sdl3Camera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFrameUpdate, IPreDraw
+{
+ public Event OnViewMatrixChanged { get; } = new();
+ public Event OnProjectionMatrixChanged { get; } = new();
+ public Event OnViewportChanged { get; } = new();
+ public Event OnZoomChanged { get; } = new();
+
+ private IWindow window = null!;
+ public ITransform2D Transform { get; private set; } = null!;
+
+ public Matrix4x4 ProjectionMatrix
+ {
+ get;
+ private set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ OnProjectionMatrixChanged.Invoke(this);
+ }
+ } = Matrix4x4.Identity;
+
+ public Matrix4x4 ViewMatrix
+ {
+ get;
+ private set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ OnViewMatrixChanged.Invoke(this);
+ }
+ } = Matrix4x4.Identity;
+
+ public float Zoom
+ {
+ get;
+ set
+ {
+ float newValue = Math.Max(0.1f, value);
+
+ if (field == newValue)
+ return;
+
+ field = newValue;
+ OnZoomChanged.Invoke(this);
+ }
+ } = 1f;
+
+ // TODO This causes delay since OnPreDraw calls assuming this is called in in Update
+ public Vector2D ScreenToWorldPosition(Vector2D screenPosition)
+ {
+ Vector2D worldPosition = Vector2D.Transform(screenPosition, Transform);
+ return worldPosition;
+ }
+
+ public Vector2D WorldToScreenPosition(Vector2D worldPosition)
+ {
+ Vector2D screenPosition = Vector2D.Transform(worldPosition, Transform);
+ return screenPosition;
+ }
+
+ public void LastActiveFrame() => Transform = null!;
+ public void FirstActiveFrame()
+ {
+ window = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour();
+ Transform = BehaviourController.GetRequiredBehaviour();
+ }
+
+ public void PreDraw()
+ {
+ ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(window.Size.X, window.Size.Y);
+
+ ViewMatrix = Matrix4x4.Identity
+ .ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y))
+ .ApplyScale(Zoom)
+ .ApplyRotationZ(-Transform.Rotation * Math.DegreeToRadian)
+ .ApplyTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f));
+ }
+}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/Sdl3GameInputs.cs b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3GameInputs.cs
new file mode 100644
index 0000000..74b1482
--- /dev/null
+++ b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3GameInputs.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+
+using Engine.Core;
+using Engine.Systems.Input;
+
+using MyUniverse.Shared.Behaviours.Abstract;
+
+using SDL3;
+
+namespace Engine.Integration.SDL3;
+
+public class Sdl3GameInputs : Behaviour, IGameInputs, IFirstFrameUpdate
+{
+ public IButtonInputs.InputEvent OnAnyButtonPressed { get; } = new();
+ public IButtonInputs.InputEvent OnAnyButtonReleased { get; } = new();
+
+ private readonly Dictionary.InputEvent> OnPressed = new(256);
+ private readonly Dictionary.InputEvent> OnReleased = new(256);
+
+ private readonly Dictionary keyMappingReversed = [];
+ private readonly Dictionary keyMapping = new() {
+ { SDL.Keycode.Space, IGameInputs.Button.Interact }
+ };
+
+ private Sdl3KeyboardInputs keyboardInputs = null!;
+
+ public void FirstActiveFrame()
+ {
+ keyboardInputs = BehaviourController.GetOrAddBehaviour();
+ keyboardInputs.OnAnyButtonPressed.AddListener(AnyPressCallback);
+ keyboardInputs.OnAnyButtonReleased.AddListener(AnyReleaseCallback);
+
+ foreach ((SDL.Keycode key, IGameInputs.Button button) in keyMapping)
+ keyboardInputs.RegisterOnPress(key, PressCallback);
+
+ foreach ((SDL.Keycode key, IGameInputs.Button button) in keyMapping)
+ keyboardInputs.RegisterOnRelease(key, ReleaseCallback);
+ }
+
+ public void ExitUniverse(IUniverse universe)
+ {
+ keyboardInputs.OnAnyButtonPressed.RemoveListener(AnyPressCallback);
+ keyboardInputs.OnAnyButtonReleased.RemoveListener(AnyReleaseCallback);
+
+ foreach ((SDL.Keycode key, IGameInputs.Button button) in keyMapping)
+ keyboardInputs.UnregisterOnPress(key, PressCallback);
+
+ foreach ((SDL.Keycode key, IGameInputs.Button button) in keyMapping)
+ keyboardInputs.UnregisterOnRelease(key, ReleaseCallback);
+ }
+
+ private void PressCallback(IButtonInputs sender, IButtonInputs.ButtonCallbackArguments args)
+ {
+ if (!keyMapping.TryGetValue(args.Button, out IGameInputs.Button button))
+ return;
+
+ if (!OnPressed.TryGetValue(button, out IButtonInputs.InputEvent? @event))
+ return;
+
+ @event.Invoke(this, new(button));
+ }
+
+ private void ReleaseCallback(IButtonInputs sender, IButtonInputs.ButtonCallbackArguments args)
+ {
+ if (!keyMapping.TryGetValue(args.Button, out IGameInputs.Button button))
+ return;
+
+ if (!OnReleased.TryGetValue(button, out IButtonInputs.InputEvent? @event))
+ return;
+
+ @event.Invoke(this, new(button));
+ }
+
+ private void AnyReleaseCallback(IButtonInputs sender, IButtonInputs.ButtonCallbackArguments args)
+ {
+ if (keyMapping.TryGetValue(args.Button, out IGameInputs.Button button))
+ OnAnyButtonPressed?.Invoke(this, new(button));
+ }
+
+ private void AnyPressCallback(IButtonInputs sender, IButtonInputs.ButtonCallbackArguments args)
+ {
+ if (keyMapping.TryGetValue(args.Button, out IGameInputs.Button button))
+ OnAnyButtonPressed?.Invoke(this, new(button));
+ }
+
+ public void RegisterOnPress(IGameInputs.Button button, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (!OnPressed.TryGetValue(button, out IButtonInputs.InputEvent? @event))
+ {
+ @event = new();
+ OnPressed.Add(button, @event);
+ }
+
+ @event.AddListener(callback);
+ }
+
+ public void UnregisterOnPress(IGameInputs.Button button, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (OnPressed.TryGetValue(button, out IButtonInputs.InputEvent? @event))
+ @event.RemoveListener(callback);
+ }
+
+ public void RegisterOnRelease(IGameInputs.Button button, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (!OnReleased.TryGetValue(button, out IButtonInputs.InputEvent? @event))
+ {
+ @event = new();
+ OnReleased.Add(button, @event);
+ }
+
+ @event.AddListener(callback);
+ }
+
+ public void UnregisterOnRelease(IGameInputs.Button button, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (OnReleased.TryGetValue(button, out IButtonInputs.InputEvent? @event))
+ @event.RemoveListener(callback);
+ }
+
+ public bool IsPressed(IGameInputs.Button button)
+ {
+ if (!keyMappingReversed.TryGetValue(button, out SDL.Keycode key))
+ throw new($"{Enum.GetName(button)} is not mapped correctly");
+
+ return keyboardInputs.IsPressed(key);
+ }
+
+ public Sdl3GameInputs()
+ {
+ foreach ((SDL.Keycode key, IGameInputs.Button button) in keyMapping)
+ keyMappingReversed.Add(button, key);
+ }
+}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/Sdl3KeyboardInputs.cs b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3KeyboardInputs.cs
new file mode 100644
index 0000000..cd75dff
--- /dev/null
+++ b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3KeyboardInputs.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+
+using Engine.Core;
+using Engine.Systems.Input;
+
+using SDL3;
+
+namespace Engine.Integration.SDL3;
+
+public class Sdl3KeyboardInputs : Behaviour, ISdl3EventProcessor, IButtonInputs
+{
+ public IButtonInputs.InputEvent OnAnyButtonPressed { get; } = new();
+ public IButtonInputs.InputEvent OnAnyButtonReleased { get; } = new();
+
+ private readonly Dictionary.InputEvent> OnPressed = new(256);
+ private readonly Dictionary.InputEvent> OnReleased = new(256);
+
+ private readonly FastList currentlyPressedKeys = [];
+
+ public void RegisterOnPress(SDL.Keycode key, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (!OnPressed.TryGetValue(key, out IButtonInputs.InputEvent? delegateCallback))
+ {
+ delegateCallback = new();
+ OnPressed.Add(key, delegateCallback);
+ }
+
+ delegateCallback.AddListener(callback);
+ }
+
+ public void UnregisterOnPress(SDL.Keycode key, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (OnPressed.TryGetValue(key, out IButtonInputs.InputEvent? delegateCallback))
+ delegateCallback.RemoveListener(callback);
+ }
+
+ public void RegisterOnRelease(SDL.Keycode key, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (!OnReleased.TryGetValue(key, out IButtonInputs.InputEvent? delegateCallback))
+ {
+ delegateCallback = new();
+ OnReleased.Add(key, delegateCallback);
+ }
+
+ delegateCallback.AddListener(callback);
+ }
+
+ public void UnregisterOnRelease(SDL.Keycode key, IButtonInputs.InputEvent.EventHandler callback)
+ {
+ if (OnReleased.TryGetValue(key, out IButtonInputs.InputEvent? delegateCallback))
+ delegateCallback.RemoveListener(callback);
+ }
+
+ public bool IsPressed(SDL.Keycode button) => currentlyPressedKeys.Contains(button);
+
+ public void Process(SDL.Event sdlEvent)
+ {
+ switch ((SDL.EventType)sdlEvent.Type)
+ {
+ case SDL.EventType.KeyDown:
+ SDL.Keycode currentlyPressedKey = sdlEvent.Key.Key;
+ if (IsPressed(currentlyPressedKey))
+ return;
+
+ currentlyPressedKeys.Add(currentlyPressedKey);
+
+ if (OnPressed.TryGetValue(currentlyPressedKey, out IButtonInputs.InputEvent? pressCallback))
+ pressCallback?.Invoke(this, new(currentlyPressedKey));
+
+ OnAnyButtonPressed?.Invoke(this, new(currentlyPressedKey));
+ return;
+
+ case SDL.EventType.KeyUp:
+ SDL.Keycode previouslyPressedKey = sdlEvent.Key.Key;
+ if (!IsPressed(previouslyPressedKey))
+ return;
+
+ currentlyPressedKeys.Remove(previouslyPressedKey);
+
+ if (OnReleased.TryGetValue(previouslyPressedKey, out IButtonInputs.InputEvent? releaseCallback))
+ releaseCallback?.Invoke(this, new(previouslyPressedKey));
+
+ OnAnyButtonReleased?.Invoke(this, new(previouslyPressedKey));
+ return;
+ }
+ }
+}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/Sdl3TriangleBatch.cs b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3TriangleBatch.cs
new file mode 100644
index 0000000..3dd64e0
--- /dev/null
+++ b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3TriangleBatch.cs
@@ -0,0 +1,269 @@
+using System;
+using Engine.Core;
+using Engine.Systems.Graphics;
+using SDL3;
+using Silk.NET.OpenGL;
+
+namespace Engine.Integration.SDL3;
+
+public class Sdl3TriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdate
+{
+ private GL gl = null!;
+ private readonly uint MaxVertices = 1024;
+ private uint vao;
+ private uint vbo;
+ private uint shader;
+
+ private uint vertexIndex = 0;
+ private VertexPositionColor[] vertices = new VertexPositionColor[1024];
+
+ private Matrix4x4 view = Matrix4x4.Identity;
+ private Matrix4x4 projection = Matrix4x4.Identity;
+ private Sdl3Window sdl3window = null!;
+ private ICamera? camera = null!;
+ private nint glContext;
+
+ public void FirstActiveFrame()
+ {
+ sdl3window = BehaviourController.GetRequiredBehaviourInParent();
+ camera = BehaviourController.GetRequiredBehaviourInParent();
+ nint window = sdl3window.Window;
+
+ SDL.GLSetAttribute(SDL.GLAttr.ContextMajorVersion, 3);
+ SDL.GLSetAttribute(SDL.GLAttr.ContextMinorVersion, 3);
+ SDL.GLSetAttribute(SDL.GLAttr.ContextProfileMask, (int)SDL.GLProfile.Core);
+ SDL.GLSetAttribute(SDL.GLAttr.DoubleBuffer, 1);
+
+ glContext = SDL.GLCreateContext(window);
+ if (glContext == nint.Zero)
+ throw new Exception(SDL.GetError());
+
+ SDL.GLMakeCurrent(window, glContext);
+
+ gl = GL.GetApi(procName => SDL.GLGetProcAddress(procName));
+ gl.Viewport(0, 0, (uint)sdl3window.Size.X, (uint)sdl3window.Size.Y);
+ vertices = new VertexPositionColor[MaxVertices];
+
+ // --- GL objects ---
+ vao = gl.GenVertexArray();
+ vbo = gl.GenBuffer();
+
+ gl.BindVertexArray(vao);
+ gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
+
+ unsafe
+ {
+ gl.BufferData(
+ BufferTargetARB.ArrayBuffer,
+ (nuint)(MaxVertices * sizeof(VertexPositionColor)),
+ null,
+ BufferUsageARB.DynamicDraw
+ );
+
+ // position
+ gl.EnableVertexAttribArray(0);
+ gl.VertexAttribPointer(
+ 0,
+ 3,
+ VertexAttribPointerType.Float,
+ false,
+ (uint)sizeof(VertexPositionColor),
+ (void*)0
+ );
+
+ // color
+ gl.EnableVertexAttribArray(1);
+ gl.VertexAttribPointer(
+ 1,
+ 4,
+ VertexAttribPointerType.Float,
+ false,
+ (uint)sizeof(VertexPositionColor),
+ (void*)(3 * sizeof(float))
+ );
+
+ gl.BindVertexArray(0);
+ }
+
+ shader = CreateShader();
+ }
+
+ public void Draw(Triangle triangle, ColorRGBA colorRGBA)
+ {
+ if (vertexIndex + 3 >= vertices.Length)
+ Flush();
+
+ Vector2D A = triangle.A;
+ Vector2D B = triangle.B;
+ Vector2D C = triangle.C;
+
+ Vector4D c = new Vector4D(colorRGBA.R, colorRGBA.G, colorRGBA.B, colorRGBA.A) / 256f;
+
+ vertices[vertexIndex++] = new()
+ {
+ Position = new Vector3D(A.X, A.Y, 0f),
+ Color = c
+ };
+
+ vertices[vertexIndex++] = new()
+ {
+ Position = new Vector3D(B.X, B.Y, 0f),
+ Color = c
+ };
+
+ vertices[vertexIndex++] = new()
+ {
+ Position = new Vector3D(C.X, C.Y, 0f),
+ Color = c
+ };
+ }
+
+ public void Begin(Matrix4x4? view = null, Matrix4x4? projection = null)
+ {
+ this.view = view ?? camera?.ViewMatrix ?? Matrix4x4.Identity;
+
+ if (projection != null)
+ this.projection = projection.Value;
+ else
+ this.projection = camera?.ProjectionMatrix ?? Matrix4x4.CreateOrthographicViewCentered(sdl3window.Size.X, sdl3window.Size.Y);
+
+ vertexIndex = 0;
+ SDL.GLMakeCurrent(sdl3window.Window, glContext);
+ gl.ClearColor(sdl3window.BackgroundColor.R / 256f, sdl3window.BackgroundColor.G / 256f, sdl3window.BackgroundColor.B / 256f, 1f);
+ gl.Clear(ClearBufferMask.ColorBufferBit);
+ }
+
+ public void End()
+ {
+ Flush();
+
+ SDL.GLSwapWindow(sdl3window.Window);
+ }
+
+ private void Flush()
+ {
+ if (vertexIndex == 0)
+ return;
+
+ gl.UseProgram(shader);
+
+ int viewLoc = gl.GetUniformLocation(shader, "uView");
+ int projLoc = gl.GetUniformLocation(shader, "uProjection");
+
+ unsafe
+ {
+ Span temp = stackalloc float[16];
+
+ CopyTo(view.Transposed, temp);
+ fixed (float* v = temp)
+ gl.UniformMatrix4(viewLoc, 1, false, v);
+
+ CopyTo(projection.Transposed, temp);
+ fixed (float* p = temp)
+ gl.UniformMatrix4(projLoc, 1, false, p);
+
+ gl.BindVertexArray(vao);
+ gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
+
+ fixed (VertexPositionColor* ptr = vertices)
+ {
+ gl.BufferSubData(
+ BufferTargetARB.ArrayBuffer,
+ 0,
+ (nuint)(vertexIndex * sizeof(VertexPositionColor)),
+ ptr
+ );
+ }
+ }
+
+ gl.DrawArrays(PrimitiveType.Triangles, 0, vertexIndex);
+
+ vertexIndex = 0;
+ }
+
+ public static void CopyTo(Matrix4x4 m, Span destination)
+ {
+ if (destination.Length < 16)
+ throw new ArgumentException("Destination span must have at least 16 elements.");
+
+ destination[0] = m.M11;
+ destination[1] = m.M12;
+ destination[2] = m.M13;
+ destination[3] = m.M14;
+
+ destination[4] = m.M21;
+ destination[5] = m.M22;
+ destination[6] = m.M23;
+ destination[7] = m.M24;
+
+ destination[8] = m.M31;
+ destination[9] = m.M32;
+ destination[10] = m.M33;
+ destination[11] = m.M34;
+
+ destination[12] = m.M41;
+ destination[13] = m.M42;
+ destination[14] = m.M43;
+ destination[15] = m.M44;
+ }
+
+ private uint CreateShader()
+ {
+ const string vertexSrc = """
+ #version 330 core
+ layout (location = 0) in vec3 aPosition;
+ layout (location = 1) in vec4 aColor;
+
+ uniform mat4 uView;
+ uniform mat4 uProjection;
+
+ out vec4 vColor;
+
+ void main()
+ {
+ gl_Position = uProjection * uView * vec4(aPosition, 1.0);
+ vColor = aColor;
+ }
+ """;
+
+ const string fragmentSrc = """
+ #version 330 core
+ in vec4 vColor;
+ out vec4 FragColor;
+
+ void main()
+ {
+ FragColor = vColor;
+ }
+ """;
+
+ uint vs = CompileShader(ShaderType.VertexShader, vertexSrc);
+ uint fs = CompileShader(ShaderType.FragmentShader, fragmentSrc);
+
+ uint program = gl.CreateProgram();
+ gl.AttachShader(program, vs);
+ gl.AttachShader(program, fs);
+ gl.LinkProgram(program);
+
+ gl.DeleteShader(vs);
+ gl.DeleteShader(fs);
+
+ return program;
+ }
+
+ private uint CompileShader(ShaderType type, string src)
+ {
+ uint shader = gl.CreateShader(type);
+ gl.ShaderSource(shader, src);
+ gl.CompileShader(shader);
+ return shader;
+ }
+
+ struct VertexPositionColor
+ {
+ public Vector3D Position;
+ public Vector4D Color;
+
+ public const int SizeInBytes = (3 + 4) * sizeof(float);
+ }
+}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/SdlWindow.cs b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3Window.cs
similarity index 67%
rename from Platforms/Desktop/Engine.Integration.SDL3/SdlWindow.cs
rename to Platforms/Desktop/Engine.Integration.SDL3/Sdl3Window.cs
index beeb3e0..8e84024 100644
--- a/Platforms/Desktop/Engine.Integration.SDL3/SdlWindow.cs
+++ b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3Window.cs
@@ -6,7 +6,7 @@ using SDL3;
namespace Engine.Integration.SDL3;
-public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniverse, IExitUniverse
+public class Sdl3Window : Behaviour, IWindow, IDisposable, IEnterUniverse, IExitUniverse
{
public Event OnStateChanged { get; } = new();
public Event OnFocusStateChanged { get; } = new();
@@ -19,26 +19,7 @@ public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniver
public Event OnSizeChanged { get; } = new();
public Event OnBackgroundColorChanged { get; } = new();
- private IntPtr window = IntPtr.Zero;
- private IntPtr renderer = IntPtr.Zero;
-
- private ulong nextRenderTick = 0;
- private ulong renderInterval = (ulong)(1f / 60f * 1000f);
-
- public uint TargetFps
- {
- get => field;
- set
- {
- if (value == field)
- return;
-
- uint previousTargetFps = field;
- field = value;
- renderInterval = value == 0 ? 0 : (ulong)(1f / value * 1000f);
- OnTargetFpsChanged.Invoke(this, new(previousTargetFps));
- }
- } = 0;
+ public IntPtr Window { get; private set; } = IntPtr.Zero;
public WindowState State
{
@@ -63,7 +44,7 @@ public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniver
public WindowFocusState FocusState
{
get => field;
- private set
+ internal set
{
if (value == field)
return;
@@ -84,26 +65,26 @@ public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniver
if (value is WindowMode.Windowed or WindowMode.BorderlessWindowed)
{
- SDL.SetWindowFullscreen(window, false);
- SDL.SyncWindow(window);
+ SDL.SetWindowFullscreen(Window, false);
+ SDL.SyncWindow(Window);
}
- if (window != IntPtr.Zero)
+ if (Window != IntPtr.Zero)
switch (value)
{
- case WindowMode.Windowed: SDL.SetWindowBordered(window, true); break;
- case WindowMode.BorderlessWindowed: SDL.SetWindowBordered(window, false); break;
- case WindowMode.BorderlessFullscreen: SDL.SetWindowFullscreen(window, true); break;
- case WindowMode.Fullscreen: SDL.SetWindowFullscreen(window, true); break;
+ case WindowMode.Windowed: SDL.SetWindowBordered(Window, true); break;
+ case WindowMode.BorderlessWindowed: SDL.SetWindowBordered(Window, false); break;
+ case WindowMode.BorderlessFullscreen: SDL.SetWindowFullscreen(Window, true); break;
+ case WindowMode.Fullscreen: SDL.SetWindowFullscreen(Window, true); break;
}
- SDL.SyncWindow(window);
+ SDL.SyncWindow(Window);
WindowMode previousMode = field;
field = value;
OnModeChanged.Invoke(this, new(previousMode));
}
- } = WindowMode.Fullscreen;
+ } = WindowMode.Windowed;
public WindowShowState ShowState
{
@@ -113,12 +94,12 @@ public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniver
if (value == field)
return;
- if (window != IntPtr.Zero)
+ if (Window != IntPtr.Zero)
switch (value)
{
- case WindowShowState.Normal: SDL.RestoreWindow(window); break;
- case WindowShowState.Minimized: SDL.MinimizeWindow(window); break;
- case WindowShowState.Maximized: SDL.MaximizeWindow(window); break;
+ case WindowShowState.Normal: SDL.RestoreWindow(Window); break;
+ case WindowShowState.Minimized: SDL.MinimizeWindow(Window); break;
+ case WindowShowState.Maximized: SDL.MaximizeWindow(Window); break;
}
WindowShowState previousShowState = field;
@@ -135,15 +116,15 @@ public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniver
if (value == field)
return;
- if (window != IntPtr.Zero)
+ if (Window != IntPtr.Zero)
switch (value)
{
case WindowResizeMode.Fixed:
- SDL.SetWindowResizable(window, false);
+ SDL.SetWindowResizable(Window, false);
break;
case WindowResizeMode.Resizable:
case WindowResizeMode.AspectLocked:
- SDL.SetWindowResizable(window, true);
+ SDL.SetWindowResizable(Window, true);
break;
}
@@ -198,26 +179,6 @@ public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniver
}
} = Vector2DInt.Zero;
- public void Update()
- {
- while (SDL.PollEvent(out SDL.Event e))
- ProcessInternalEvents((SDL.EventType)e.Type);
-
- ulong currentTick = SDL.GetTicks();
-
- if (currentTick >= nextRenderTick)
- {
- nextRenderTick = currentTick + renderInterval;
-
- SDL.SetRenderDrawColor(renderer, BackgroundColor.R, BackgroundColor.G, BackgroundColor.B, 255);
- SDL.RenderClear(renderer);
-
- // TODO Render
-
- SDL.RenderPresent(renderer);
- }
- }
-
public void ExitUniverse(IUniverse universe) => CloseWindow();
public void EnterUniverse(IUniverse universe)
{
@@ -242,43 +203,22 @@ public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniver
if (!SDL.Init(SDL.InitFlags.Video))
throw new Exception($"SDL_Init failed: {SDL.GetError()}");
- if (!SDL.CreateWindowAndRenderer(Title, Size.X, Size.Y, windowFlags, out window, out renderer))
+ Window = SDL.CreateWindow(Title, Size.X, Size.Y, windowFlags | SDL.WindowFlags.OpenGL);
+ if (Window == IntPtr.Zero)
throw new Exception($"SDL_CreateWindowAndRenderer failed: {SDL.GetError()}");
}
private void CloseWindow()
{
- if (renderer != IntPtr.Zero) SDL.DestroyRenderer(renderer);
- if (window != IntPtr.Zero) SDL.DestroyWindow(window);
+ if (Window != IntPtr.Zero) SDL.DestroyWindow(Window);
- renderer = IntPtr.Zero;
- window = IntPtr.Zero;
- }
-
- private void ProcessInternalEvents(SDL.EventType eventType)
- {
- switch (eventType)
- {
- case SDL.EventType.WindowRestored: ShowState = WindowShowState.Normal; break;
- case SDL.EventType.WindowMaximized: ShowState = WindowShowState.Maximized; break;
- case SDL.EventType.WindowMinimized: ShowState = WindowShowState.Minimized; break;
-
- case SDL.EventType.WindowFocusGained: FocusState = WindowFocusState.Focused; break;
- case SDL.EventType.WindowFocusLost: FocusState = WindowFocusState.Unfocused; break;
-
- case SDL.EventType.Quit:
- State = WindowState.Closed;
- BehaviourController.RemoveBehaviour(this);
- break;
- }
+ Window = IntPtr.Zero;
}
public void Dispose()
{
CloseWindow();
- // SDL.Quit(); // TODO This possibly prevents multi window
-
GC.SuppressFinalize(this);
}
}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/Sdl3WindowManager.cs b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3WindowManager.cs
new file mode 100644
index 0000000..d77b602
--- /dev/null
+++ b/Platforms/Desktop/Engine.Integration.SDL3/Sdl3WindowManager.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using Engine.Core;
+
+using SDL3;
+
+namespace Engine.Integration.SDL3;
+
+public class Sdl3WindowManager : Behaviour, IUpdate, IEnterUniverse, IExitUniverse
+{
+ private readonly BehaviourCollector windows = new();
+ private readonly BehaviourCollector eventProcessors = new();
+ private readonly Dictionary windowsDictionary = [];
+
+ public Sdl3WindowManager()
+ {
+ windows.OnCollected.AddListener(OnWindowCollected);
+ windows.OnRemoved.AddListener(OnWindowRemoved);
+ }
+
+ private void OnWindowCollected(IBehaviourCollector sender, IBehaviourCollector.BehaviourCollectedArguments args)
+ => windowsDictionary.Add(args.BehaviourCollected.Window, args.BehaviourCollected);
+
+ private void OnWindowRemoved(IBehaviourCollector sender, IBehaviourCollector.BehaviourRemovedArguments args)
+ {
+ windowsDictionary.Remove(args.BehaviourRemoved.Window);
+ if (windowsDictionary.Count == 0)
+ SDL.Quit();
+ }
+
+ public void EnterUniverse(IUniverse universe)
+ {
+ windows.Assign(universe);
+ eventProcessors.Assign(universe);
+ }
+
+ public void ExitUniverse(IUniverse universe)
+ {
+ windows.Unassign();
+ eventProcessors.Unassign();
+ }
+
+ public void Update()
+ {
+ while (SDL.PollEvent(out SDL.Event e))
+ ProcessInternalEvents(e);
+ }
+
+ private void ProcessInternalEvents(SDL.Event sdlEvent)
+ {
+ if (!windowsDictionary.TryGetValue(SDL.GetWindowFromID(sdlEvent.Window.WindowID), out Sdl3Window? window))
+ return;
+
+ for (int i = 0; i < eventProcessors.Count; i++)
+ eventProcessors[i].Process(sdlEvent);
+
+ SDL.EventType eventType = (SDL.EventType)sdlEvent.Type;
+
+ switch (eventType)
+ {
+ case SDL.EventType.WindowRestored: window.ShowState = WindowShowState.Normal; break;
+ case SDL.EventType.WindowMaximized: window.ShowState = WindowShowState.Maximized; break;
+ case SDL.EventType.WindowMinimized: window.ShowState = WindowShowState.Minimized; break;
+
+ case SDL.EventType.WindowFocusGained: window.FocusState = WindowFocusState.Focused; break;
+ case SDL.EventType.WindowFocusLost: window.FocusState = WindowFocusState.Unfocused; break;
+
+ case SDL.EventType.WindowCloseRequested:
+ case SDL.EventType.Quit: Universe.Remove(window.UniverseObject); break;
+ }
+ }
+}
diff --git a/Platforms/Desktop/Engine.Integration.SDL3/Windowing/IWindow.cs b/Platforms/Desktop/Engine.Integration.SDL3/Windowing/IWindow.cs
index f6b54e5..be77cb8 100644
--- a/Platforms/Desktop/Engine.Integration.SDL3/Windowing/IWindow.cs
+++ b/Platforms/Desktop/Engine.Integration.SDL3/Windowing/IWindow.cs
@@ -22,7 +22,6 @@ public interface IWindow : IBehaviour
WindowResizeMode ResizeMode { get; set; }
string Title { get; set; }
- uint TargetFps { get; set; }
Vector2DInt Size { get; set; }
ColorRGB BackgroundColor { get; set; }
diff --git a/Platforms/Desktop/Program.cs b/Platforms/Desktop/Program.cs
index fcad1d1..e92e3db 100644
--- a/Platforms/Desktop/Program.cs
+++ b/Platforms/Desktop/Program.cs
@@ -7,6 +7,7 @@ using Engine.Integration.SDL3;
using Engine.Serializers.Yaml;
using Engine.Systems.Network;
using Engine.Systems.Time;
+using MyUniverse.Shared.Behaviours;
Universe universe = new();
@@ -43,22 +44,51 @@ desktopParent.AddChild(universe.InstantiateUniverseObject().SetUniverseObject("T
Sdl3Window window = Engine.Core.Factory.BehaviourFactory.Instantiate();
window.Title = "MyUniverse";
window.Size = new(800, 600);
-window.Mode = WindowMode.BorderlessWindowed;
+window.Mode = WindowMode.Windowed;
+
+Sdl3Window window2 = Engine.Core.Factory.BehaviourFactory.Instantiate();
+window2.Title = "MyUniverse 2";
+window2.Size = new(400, 300);
+window2.Mode = WindowMode.Windowed;
+
+universe.InstantiateUniverseObject().SetUniverseObject("SDL3 Manager", desktopParent)
+ .BehaviourController.AddBehaviour()
+ .BehaviourController.AddBehaviour()
+ .BehaviourController.AddBehaviour();
IUniverseObject windowUO = universe.InstantiateUniverseObject().SetUniverseObject("SDL3 Window", desktopParent);
+windowUO.BehaviourController.AddBehaviour();
+windowUO.BehaviourController.AddBehaviour(Shape2D.Square, new ColorRGB(0, 0, 128));
windowUO.BehaviourController.AddBehaviour(window);
+windowUO.BehaviourController.AddBehaviour().Zoom = 20f;
+windowUO.BehaviourController.AddBehaviour();
+windowUO.BehaviourController.AddBehaviour();
+
+IUniverseObject windowUO2 = universe.InstantiateUniverseObject().SetUniverseObject("SDL3 Window 2", desktopParent);
+windowUO2.BehaviourController.AddBehaviour().Rotation = 45f;
+windowUO2.BehaviourController.AddBehaviour(Shape2D.Square, new ColorRGB(128, 0, 0));
+windowUO2.BehaviourController.AddBehaviour(window2);
+windowUO2.BehaviourController.AddBehaviour().Zoom = 20f;
+windowUO2.BehaviourController.AddBehaviour();
+windowUO2.BehaviourController.AddBehaviour();
// Testing Start
-TickerStopwatch tickerStopwatch = windowUO.BehaviourController.AddBehaviour();
-tickerStopwatch.Period = 5f;
-tickerStopwatch.OnTick.AddListener(sender =>
-{
- if (window.IsInitialized)
- windowUO.BehaviourController.RemoveBehaviour(window);
- else
- windowUO.BehaviourController.AddBehaviour(window);
-});
-tickerStopwatch.Start();
+// TickerStopwatch tickerStopwatch = windowUO.BehaviourController.AddBehaviour();
+// tickerStopwatch.Period = 5f;
+// tickerStopwatch.OnTick.AddListener(sender =>
+// {
+// if (window.IsInitialized)
+// windowUO.BehaviourController.RemoveBehaviour(window);
+// else
+// windowUO.BehaviourController.AddBehaviour(window);
+// });
+// tickerStopwatch.Start();
+
+// universe.InstantiateUniverseObject()
+// .SetUniverseObject("Square")
+// .BehaviourController.AddBehaviour()
+// .BehaviourController.AddBehaviour(Shape2D.Square, new ColorRGB(255, 255, 0));
+
// Testing End
universe.Initialize();
@@ -82,6 +112,7 @@ while (true)
timeSinceStart += updateTimeSpan;
universe.Update(new(timeSinceStart, updateTimeSpan));
+ universe.Draw();
lastRun = now;
System.Threading.Thread.Sleep(1);