From dca2275c4d11b108a1156fdcfccd7e82a047879f Mon Sep 17 00:00:00 2001 From: copygirl Date: Tue, 22 Nov 2022 17:42:39 +0100 Subject: [PATCH] Input overhaul + ImGui --- src/FastNoiseLite | 2 +- src/Immersion/Program.cs | 16 +- src/flecs-cs | 2 +- src/gaemstone.Client/Color.cs | 26 +-- .../Components/InputComponents.cs | 90 ++++++++ .../Systems/FreeCameraController.cs | 70 ++++--- .../Systems/ImGuiInputDebug.cs | 193 ++++++++++++++++++ src/gaemstone.Client/Systems/ImGuiManager.cs | 120 +++++++++++ src/gaemstone.Client/Systems/Input.cs | 83 -------- src/gaemstone.Client/Systems/InputManager.cs | 141 +++++++++++++ src/gaemstone.Client/Systems/Renderer.cs | 10 +- .../Systems/TextureManager.cs | 8 +- src/gaemstone.Client/Systems/Windowing.cs | 5 +- src/gaemstone.Client/gaemstone.Client.csproj | 1 + src/gaemstone/ECS/System.cs | 2 +- src/gaemstone/ECS/Universe+Modules.cs | 10 +- 16 files changed, 633 insertions(+), 146 deletions(-) create mode 100644 src/gaemstone.Client/Components/InputComponents.cs create mode 100644 src/gaemstone.Client/Systems/ImGuiInputDebug.cs create mode 100644 src/gaemstone.Client/Systems/ImGuiManager.cs delete mode 100644 src/gaemstone.Client/Systems/Input.cs create mode 100644 src/gaemstone.Client/Systems/InputManager.cs diff --git a/src/FastNoiseLite b/src/FastNoiseLite index 5923df5..95900f7 160000 --- a/src/FastNoiseLite +++ b/src/FastNoiseLite @@ -1 +1 @@ -Subproject commit 5923df5d822f7610100d0e77f629c607ed64934a +Subproject commit 95900f7372d9aad1691cfeabf45103a132a4664f diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs index bf55354..d522861 100644 --- a/src/Immersion/Program.cs +++ b/src/Immersion/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Globalization; using System.Threading; @@ -13,7 +13,6 @@ using static gaemstone.Client.Components.CameraComponents; using static gaemstone.Client.Components.RenderingComponents; using static gaemstone.Client.Components.ResourceComponents; using static gaemstone.Client.Systems.FreeCameraController; -using static gaemstone.Client.Systems.Input; using static gaemstone.Client.Systems.Windowing; using static gaemstone.Components.TransformComponents; @@ -39,23 +38,26 @@ window.Center(); // universe.Modules.Register(); universe.Modules.Register(); -game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window))); -game.Set(new GameWindow(window)); - universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); +universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); -universe.Modules.Register(); -game.Set(new RawInput()); +universe.Modules.Register(); +universe.Modules.Register(); +universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); + +game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window))); +game.Set(new GameWindow(window)); + universe.New("MainCamera") .Set(Camera.Default3D) .Set((GlobalTransform) Matrix4X4.CreateTranslation(0.0F, 2.0F, 0.0F)) diff --git a/src/flecs-cs b/src/flecs-cs index 3f9cf9c..2e82db1 160000 --- a/src/flecs-cs +++ b/src/flecs-cs @@ -1 +1 @@ -Subproject commit 3f9cf9c3793337eabf8647db6a4ac44017f20cc3 +Subproject commit 2e82db165948e073b813ac712bedd00c70627d03 diff --git a/src/gaemstone.Client/Color.cs b/src/gaemstone.Client/Color.cs index ba7bc4d..721c857 100644 --- a/src/gaemstone.Client/Color.cs +++ b/src/gaemstone.Client/Color.cs @@ -10,13 +10,12 @@ namespace gaemstone.Client; public readonly struct Color : IEquatable { - public static Color Transparent { get; } = default; - - public static Color Black { get; } = FromRGB(0x000000); - public static Color White { get; } = FromRGB(0xFFFFFF); + public static readonly Color Transparent = default; + public static readonly Color Black = FromRGB(0x000000); + public static readonly Color White = FromRGB(0xFFFFFF); [FieldOffset(0)] - public readonly uint Value; + public readonly uint RGBA; [FieldOffset(0)] public readonly byte R; @@ -27,25 +26,28 @@ public readonly struct Color [FieldOffset(3)] public readonly byte A; - private Color(uint value) - { Unsafe.SkipInit(out this); Value = value; } + private Color(uint rgba) + { Unsafe.SkipInit(out this); RGBA = rgba; } private Color(byte r, byte g, byte b, byte a) { Unsafe.SkipInit(out this); R = r; G = g; B = b; A = a; } public static Color FromRGBA(uint rgba) => new(rgba); - public static Color FromRGB(uint rgb) => new(rgb | 0xFF000000); - public static Color FromRGBA(byte r, byte g, byte b, byte a) => new(r, g, b, a); + + public static Color FromRGB(uint rgb) => new(rgb | 0xFF000000); public static Color FromRGB(byte r, byte g, byte b) => new(r, g, b, 0xFF); + public static Color FromGrayscale(byte gray) => new(gray, gray, gray, 0xFF); + public static Color FromGrayscale(byte gray, byte alpha) => new(gray, gray, gray, alpha); + public bool Equals(Color other) - => Value == other.Value; + => RGBA == other.RGBA; public override bool Equals([NotNullWhen(true)] object? obj) => (obj is Color color) && Equals(color); public override int GetHashCode() - => Value.GetHashCode(); + => RGBA.GetHashCode(); public override string? ToString() - => $"Color(0x{Value:X8})"; + => $"Color(0x{RGBA:X8})"; public static bool operator ==(Color left, Color right) => left.Equals(right); public static bool operator !=(Color left, Color right) => !left.Equals(right); diff --git a/src/gaemstone.Client/Components/InputComponents.cs b/src/gaemstone.Client/Components/InputComponents.cs new file mode 100644 index 0000000..cc24c09 --- /dev/null +++ b/src/gaemstone.Client/Components/InputComponents.cs @@ -0,0 +1,90 @@ +using System; +using System.Numerics; +using gaemstone.ECS; + +namespace gaemstone.Client.Components; + +[Module] +public class InputComponents +{ + [Entity(Global = true)] + [Add] + public struct Input { } + + [Entity("Input", "Mouse", Global = true)] + [Add] + public struct Mouse { } + + [Entity("Input", "Keyboard", Global = true)] + [Add] + public struct Keyboard { } + + [Tag] + public struct Gamepad { } + + + /// Present on inputs / actions that are currently active. + [Component] public struct Active { public TimeSpan Duration; } + + /// Present on inputs / actions were activated this frame. + [Tag] public struct Activated { } + + /// Present on inputs / actions were deactivated this frame. + [Tag] public struct Deactivated { } + + + /// + /// Relationship on which indicates keyboard + /// and gamepad input is currently captured by the target. + /// + /// + /// This is set if a UI element is focused that captures + /// navigational or text input. + /// + [Tag, Relation, Exclusive] + public struct InputCapturedBy { } + + /// + /// Relationship on which indicates that mouse + /// input (buttons and wheel) is currently captured by the target. + /// + /// + /// This could for example include the mouse currently being over + /// a UI element, preventing the game from handling mouse input. + /// + [Tag, Relation, Exclusive] + public struct MouseInputCapturedBy { } + + /// + /// Relationship on which indicates that the + /// cursor is currently captured by the target, and thus hidden. + /// + /// + /// This is set when a camera controller assumes control of the mouse. + /// + [Tag, Relation, Exclusive] + [With] + [With] + public struct CursorCapturedBy { } + + + [Private, Component] + public readonly struct RawValue1D + { + private readonly float _value; + private RawValue1D(float value) => _value = value; + + public static implicit operator float(RawValue1D value) => value._value; + public static implicit operator RawValue1D(float value) => new(value); + } + + [Private, Component] + public readonly struct RawValue2D + { + private readonly Vector2 _value; + private RawValue2D(Vector2 value) => _value = value; + + public static implicit operator Vector2(RawValue2D value) => value._value; + public static implicit operator RawValue2D(Vector2 value) => new(value); + } +} diff --git a/src/gaemstone.Client/Systems/FreeCameraController.cs b/src/gaemstone.Client/Systems/FreeCameraController.cs index ea2a68d..1296e4b 100644 --- a/src/gaemstone.Client/Systems/FreeCameraController.cs +++ b/src/gaemstone.Client/Systems/FreeCameraController.cs @@ -1,16 +1,16 @@ using System; +using System.Numerics; using gaemstone.ECS; -using Silk.NET.Input; using Silk.NET.Maths; using static gaemstone.Client.Components.CameraComponents; -using static gaemstone.Client.Systems.Input; +using static gaemstone.Client.Components.InputComponents; using static gaemstone.Components.TransformComponents; namespace gaemstone.Client.Systems; [Module] [DependsOn] -[DependsOn] +[DependsOn] [DependsOn] public class FreeCameraController { @@ -18,41 +18,59 @@ public class FreeCameraController public struct CameraController { public float MouseSensitivity { get; set; } - public Vector2D? MouseGrabbedAt { get; set; } } [System] - public static void UpdateCamera(TimeSpan delta, in Camera camera, - ref GlobalTransform transform, ref CameraController controller, - [Game] RawInput input) + public static void UpdateCamera( + Universe universe, TimeSpan delta, + in Camera camera, ref GlobalTransform transform, ref CameraController controller) { - var isMouseDown = input.IsDown(MouseButton.Right); - var isMouseGrabbed = controller.MouseGrabbedAt != null; - if (isMouseDown != isMouseGrabbed) { - if (isMouseDown) controller.MouseGrabbedAt = input.MousePosition; - else controller.MouseGrabbedAt = null; - } + var input = universe.Lookup(); + var mouse = universe.Lookup(); + var keyboard = universe.Lookup(); + if ((input == null) || (mouse == null) || (keyboard == null)) return; + + var module = universe.LookupOrThrow(); + var capturedBy = input.GetTarget(); + var inputCapturedBy = input.GetTarget(); + var isCaptured = (capturedBy != null); + // If another system has the mouse captured, don't do anything here. + if (isCaptured && (capturedBy != module)) return; - var mouseMoved = Vector2D.Zero; - if (controller.MouseGrabbedAt is Vector2D pos) { - mouseMoved = input.MousePosition - pos; - input.Context!.Mice[0].Position = pos.ToSystem(); + var isMouseDown = ((inputCapturedBy == null) || (inputCapturedBy == module)) + && mouse.Lookup("Buttons/Right")?.Has() == true; + if (isMouseDown != isCaptured) { + if (isMouseDown) + input.Add(module); + else { + input.Remove(module); + input.Remove(module); + input.Remove(module); + } } - var dt = (float)delta.TotalSeconds; - var xMovement = mouseMoved.X * dt * controller.MouseSensitivity; - var yMovement = mouseMoved.Y * dt * controller.MouseSensitivity; + var mouseMovement = Vector2.Zero; + if (isCaptured) { + var raw = (Vector2?)mouse.Lookup("Delta")?.Get() ?? default; + mouseMovement = raw * controller.MouseSensitivity * (float)delta.TotalSeconds; + } if (camera.IsOrthographic) { - transform *= Matrix4X4.CreateTranslation(-xMovement, -yMovement, 0); + transform *= Matrix4X4.CreateTranslation(-mouseMovement.X, -mouseMovement.Y, 0); } else { - var speed = dt * (input.IsDown(Key.ShiftLeft) ? 12 : 4); - var forwardMovement = ((input.IsDown(Key.W) ? -1 : 0) + (input.IsDown(Key.S) ? 1 : 0)) * speed; - var sideMovement = ((input.IsDown(Key.A) ? -1 : 0) + (input.IsDown(Key.D) ? 1 : 0)) * speed; + var shift = keyboard.Lookup("ShiftLeft")?.Has() == true; + var w = keyboard.Lookup("W")?.Has() == true; + var a = keyboard.Lookup("A")?.Has() == true; + var s = keyboard.Lookup("S")?.Has() == true; + var d = keyboard.Lookup("D")?.Has() == true; + + var speed = (shift ? 12 : 4) * (float)delta.TotalSeconds; + var forwardMovement = ((w ? -1 : 0) + (s ? 1 : 0)) * speed; + var sideMovement = ((a ? -1 : 0) + (d ? 1 : 0)) * speed; var curTranslation = new Vector3D(transform.Value.M41, transform.Value.M42, transform.Value.M43); - var yawRotation = Matrix4X4.CreateRotationY(-xMovement / 100, curTranslation); - var pitchRotation = Matrix4X4.CreateRotationX(-yMovement / 100); + var yawRotation = Matrix4X4.CreateRotationY(-mouseMovement.X / 100, curTranslation); + var pitchRotation = Matrix4X4.CreateRotationX(-mouseMovement.Y / 100); var translation = Matrix4X4.CreateTranslation(sideMovement, 0, forwardMovement); transform = translation * pitchRotation * transform * yawRotation; diff --git a/src/gaemstone.Client/Systems/ImGuiInputDebug.cs b/src/gaemstone.Client/Systems/ImGuiInputDebug.cs new file mode 100644 index 0000000..dbb02df --- /dev/null +++ b/src/gaemstone.Client/Systems/ImGuiInputDebug.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using gaemstone.ECS; +using ImGuiNET; +using Silk.NET.GLFW; +using Silk.NET.Input; +using static gaemstone.Client.Components.InputComponents; +using static gaemstone.Client.Systems.ImGuiManager; + +namespace gaemstone.Client.Systems; + +[Module] +[DependsOn] +[DependsOn] +public class ImGuiInputDebug +{ + [System] + public static void ShowInputDebugWindow(Universe universe, ImGuiData _) + { + var input = universe.Lookup(); + if (input == null) return; + + ImGui.Begin("Input Information", ImGuiWindowFlags.NoResize); + + if (universe.Lookup() is EntityRef keyboard) + DrawKeyboard(keyboard); + + if (universe.Lookup() is EntityRef mouse) { + ImGui.BeginChild("Mouse Info", new(160, 180), true); + + ImGui.Text("Position: " + (Vector2?)mouse.Lookup("Position")?.MaybeGet()); + ImGui.Text("Delta: " + (Vector2?)mouse.Lookup("Delta" )?.MaybeGet()); + ImGui.Text("Wheel: " + (float?) mouse.Lookup("Wheel" )?.MaybeGet()); + + ImGui.Spacing(); + + var buttons = mouse.Lookup("Buttons")?.GetChildren().ToArray() ?? Array.Empty(); + ImGui.Text("Buttons: " + string.Join(" ", buttons + .Where (button => button.Has()) + .Select(button => $"{button.Name} ({button.Get().Duration.TotalSeconds:f2}s)"))); + ImGui.Text(" Pressed: " + string.Join(" ", buttons + .Where (button => button.Has()) + .Select(button => button.Name))); + ImGui.Text(" Released: " + string.Join(" ", buttons + .Where (button => button.Has()) + .Select(button => button.Name))); + + ImGui.EndChild(); + } + + for (var index = 0; input.Lookup("Gamepad" + index) is EntityRef gamepad; index++) { + ImGui.SameLine(); + ImGui.BeginChild($"{gamepad.Name} Info", new(160, 180), true); + + var buttons = gamepad.Lookup("Buttons")?.GetChildren().ToArray() ?? Array.Empty(); + ImGui.Text("Buttons: " + string.Join(" ", buttons.Where(b => b.Has()) + .Select(b => $"{b.Name} ({b.Get().Duration.TotalSeconds:f2}s)"))); + ImGui.Text(" Pressed: " + string.Join(" ", buttons.Where(b => b.Has()).Select(b => b.Name))); + ImGui.Text(" Released: " + string.Join(" ", buttons.Where(b => b.Has()).Select(b => b.Name))); + + ImGui.Spacing(); + + ImGui.Text("Triggers:"); + for (var i = 0; gamepad.Lookup("Trigger" + i) is EntityRef trigger; i++) { + var text = $" {i}: {(float?)trigger.MaybeGet() ?? default:f2}"; + if (trigger.Has()) text += " pressed!"; + else if (trigger.Has()) text += " released!"; + else if (trigger.MaybeGet() is Active active) + text += $" ({active.Duration.TotalSeconds:f2}s)"; + ImGui.Text(text); + } + + ImGui.Text("Thumbsticks:"); + for (var i = 0; gamepad.Lookup("Thumbstick" + i) is EntityRef thumbstick; i++) + ImGui.Text($" {i}: {(Vector2?)thumbstick.MaybeGet() ?? default:f2}"); + + ImGui.EndChild(); + } + + ImGui.End(); + } + + private const float U = 1.00F; + private const float SM = 1.25F; + // Spacing (invisible) + private const float I = -0.50F; + private const float _ = -1.00F; + private const float ER = -1.25F; + // Special + private const float T = -11.00F; + private const float ENT = -11.50F; + private static readonly float[][] KeyboardLayout = { + new[] { U, _, U, U, U, U,I,U, U, U, U,I, U, U, U, U, I,U, U, U }, + new[] { U, U, U, U, U, U, U, U, U, U, U, U, U, 2.0F, I,U, U, U,I, U, U, U, U }, + new[] { 1.5F, U, U, U, U, U, U, U, U, U, U, U, U, ENT, I,U, U, U,I, U, U, U, T }, + new[] { 1.75F, U, U, U, U, U, U, U, U, U, U, U, U, ER, I,_, _, _,I, U, U, U, _ }, + new[] { SM, U, U, U, U, U, U, U, U, U, U, U, 2.75F, I,_, U, _,I, U, U, U, T }, + new[] { SM, SM, SM, 6.25F, SM, SM, SM, SM, I,U, U, U,I, 2.0F, U, _ }, + }; + private static readonly Key?[][] KeyboardKeys = { + new Key?[] { Key.Escape, null, Key.F1, Key.F2, Key.F3, Key.F4, null, Key.F5, Key.F6, Key.F7, Key.F8, null, Key.F9, Key.F10, Key.F11, Key.F12, null, Key.PrintScreen, Key.ScrollLock, Key.Pause }, + new Key?[] { Key.GraveAccent, Key.Number1, Key.Number2, Key.Number3, Key.Number4, Key.Number5, Key.Number6, Key.Number7, Key.Number8, Key.Number9, Key.Number0, Key.Minus, Key.Equal, Key.Backspace, null, Key.Insert, Key.Home, Key.PageUp, null, Key.NumLock, Key.KeypadDivide, Key.KeypadMultiply, Key.KeypadSubtract }, + new Key?[] { Key.Tab, Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P, Key.LeftBracket, Key.RightBracket, Key.Enter, null, Key.Delete, Key.End, Key.PageDown, null, Key.Keypad7, Key.Keypad8, Key.Keypad9, Key.KeypadAdd }, + new Key?[] { Key.CapsLock, Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L, Key.Semicolon, Key.Apostrophe, Key.BackSlash, null, null, null, null, null, null, Key.Keypad4, Key.Keypad5, Key.Keypad6, null }, + new Key?[] { Key.ShiftLeft, Key.World1, Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M, Key.Comma, Key.Period, Key.Slash, Key.ShiftRight, null, null, Key.Up, null, null, Key.Keypad1, Key.Keypad2, Key.Keypad3, Key.KeypadEnter }, + new Key?[] { Key.ControlLeft, Key.SuperLeft, Key.AltLeft, Key.Space, Key.AltRight, Key.SuperRight, Key.Menu, Key.ControlRight, null, Key.Left, Key.Down, Key.Right, null, Key.Keypad0, Key.KeypadDecimal, null }, + }; + private static readonly Dictionary KeyToNameMapping = new() { + [Key.Escape] = "Ecs", [Key.PrintScreen] = "Prn\nScr", [Key.ScrollLock] = "Scr\nLck", [Key.Pause] = "Pause", + [Key.F1] = "F1", [Key.F2] = "F2", [Key.F3] = "F3", [Key.F4] = "F4", [Key.F5] = "F5", [Key.F6] = "F6", + [Key.F7] = "F7", [Key.F8] = "F8", [Key.F9] = "F9", [Key.F10] = "F10", [Key.F11] = "F11", [Key.F12] = "F12", + [Key.Tab] = "Tab", [Key.CapsLock] = "Caps\nLock", [Key.Menu] = "Menu", + [Key.Backspace] = "Backspace", [Key.Enter] = "Enter", + + [Key.ControlLeft] = "Ctrl", [Key.ControlRight] = "Ctrl", + [Key.ShiftLeft] = "Shift", [Key.ShiftRight] = "Shift", + [Key.AltLeft] = "Alt", [Key.AltRight] = "Alt", + [Key.SuperLeft] = "Super", [Key.SuperRight] = "Super", + + [Key.Insert] = "Ins", [Key.Delete] = "Del", + [Key.Home] = "Home", [Key.End] = "End", + [Key.PageUp] = "PgUp", [Key.PageDown] = "PgDn", + [Key.NumLock] = "Num\nLck", [Key.KeypadEnter] = "Enter", + }; + + public static void DrawKeyboard(EntityRef keyboard) + { + var GLFW = Glfw.GetApi(); + const float UnitKeySize = 32.0F; + Vector2 Size = new Vector2(23, 6.5F) * UnitKeySize; + + Vector2 Border = new(1, 1); + Vector2 LabelOffset = new(7, 3); + Vector2 FaceStartOffset = new(5, 3); + Vector2 FaceEndOffset = new(5, 6); + + uint BorderColor = Color.FromGrayscale(24).RGBA; + uint LabelColor = Color.FromGrayscale(64).RGBA; + + var draw = ImGui.GetWindowDrawList(); + var offset = ImGui.GetCursorScreenPos(); + var current = Vector2.Zero; + foreach (var (widths, keys) in KeyboardLayout.Zip(KeyboardKeys)) { + foreach (var (width, key) in widths.Zip(keys)) { + uint KeyColor(byte lightness) + => (key != null) && (keyboard.Lookup(key.Value.ToString())?.Has() == true) + ? Color.FromRGB(lightness, (byte)(lightness / 2), (byte)(lightness / 2)).RGBA + : Color.FromGrayscale(lightness).RGBA; + + var start = offset + current; + var keySize = new Vector2(width, 1.0F); + + if (width == T) keySize = new Vector2((-keySize.X - 10), 2.0F); + else if (width < -10) keySize = new Vector2((-keySize.X - 10), 1.0F); + else if (width < 0) { current += new Vector2(-keySize.X * UnitKeySize, 0); continue; } + + var label = (key != null) ? KeyToNameMapping.GetValueOrDefault(key.Value) + ?? GLFW.GetKeyName((int)key.Value, 0)?.ToUpper() : null; + + var end = start + keySize * UnitKeySize; + if (width == ENT) { + var start2 = start + new Vector2(0.25F * UnitKeySize, 0); + var end2 = end + new Vector2(0, 1 * UnitKeySize); + draw.AddRectFilled(start , end , BorderColor, 3); + draw.AddRectFilled(start2 , end2 , BorderColor, 3); + draw.AddRectFilled(start + Border, end - Border, KeyColor(204), 3); + draw.AddRectFilled(start2 + Border, end2 - Border, KeyColor(204), 3); + var faceStart = start + FaceStartOffset; + var faceEnd = end - FaceEndOffset; + draw.AddRectFilled(faceStart , faceEnd , KeyColor(252), 2); + var faceStart2 = start2 + FaceStartOffset; + var faceEnd2 = end2 - FaceEndOffset; + draw.AddRectFilled(faceStart2, faceEnd2, KeyColor(252), 2); + if (label != null) draw.AddText(start + LabelOffset, LabelColor, label); + } else { + draw.AddRectFilled(start , end , BorderColor, 3); + draw.AddRectFilled(start + Border, end - Border, KeyColor(204), 3); + var faceStart = start + FaceStartOffset; + var faceEnd = end - FaceEndOffset; + draw.AddRectFilled(faceStart, faceEnd, KeyColor(252), 2); + if (label != null) draw.AddText(start + LabelOffset, LabelColor, label); + } + current += new Vector2(keySize.X * UnitKeySize, 0); + + } + current = new Vector2(0, current.Y + UnitKeySize); + if (widths == KeyboardLayout[0]) current += new Vector2(0, UnitKeySize / 2); + } + ImGui.Dummy(Size); + } +} diff --git a/src/gaemstone.Client/Systems/ImGuiManager.cs b/src/gaemstone.Client/Systems/ImGuiManager.cs new file mode 100644 index 0000000..bd12c7d --- /dev/null +++ b/src/gaemstone.Client/Systems/ImGuiManager.cs @@ -0,0 +1,120 @@ +using System; +using gaemstone.ECS; +using gaemstone.Flecs; +using ImGuiNET; +using Silk.NET.Input; +using Silk.NET.OpenGL.Extensions.ImGui; +using static gaemstone.Client.Components.InputComponents; +using static gaemstone.Client.Systems.Windowing; + +namespace gaemstone.Client.Systems; + +[Module] +[DependsOn] +[DependsOn] +[DependsOn] +public class ImGuiManager +{ + [Entity, Add] + [DependsOn] + public struct ImGuiUpdatePhase { } + + [Entity, Add] + [DependsOn] + public struct ImGuiRenderPhase { } + + [Component, Singleton] + public class ImGuiData + { + public ImGuiController Controller { get; } + internal ImGuiData(ImGuiController controller) => Controller = controller; + } + + [System] + public unsafe void Initialize(Universe universe, GameWindow window, Canvas canvas, + [Source] IInputContext inputContext, [Not] ImGuiData _) + => universe.LookupOrThrow().Set(new ImGuiData( + new(canvas.GL, window.Handle, inputContext, () => { + var IO = ImGui.GetIO(); + + // Do not save a settings or log file. + IO.NativePtr->IniFilename = null; + IO.NativePtr->LogFilename = null; + + IO.BackendFlags |= ImGuiBackendFlags.HasMouseCursors + | ImGuiBackendFlags.HasSetMousePos; + IO.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard + | ImGuiConfigFlags.NavEnableSetMousePos; + IO.ConfigWindowsResizeFromEdges = true; + + // Set up key mappings. + foreach (var imguiKey in Enum.GetValues()) { + var name = imguiKey.ToString(); + + // Adjust ImGuiKey enum names to match Silk.NET.Input.Key enum. + if (name.StartsWith("_")) name = name.Replace("_", "Number"); + if (name.EndsWith("Arrow")) name = name[..^"Arrow".Length]; + if (name.EndsWith("Ctrl" )) name = name.Replace("Ctrl", "Control"); + + if (!name.EndsWith("Bracket")) { // Leave "LeftBracket" and "RightBracket" alone. + if (name.StartsWith("Left" )) name = name["Left" .Length..] + "Left"; + if (name.StartsWith("Right")) name = name["Right".Length..] + "Right"; + } + + if (Enum.TryParse(name, true, out var silkKey)) + IO.KeyMap[(int)imguiKey] = (int)silkKey; + } + }))); + + [System] + public static void UpdateMouse(Universe universe, + [Source] IMouse impl, ImGuiData _) + { + var input = universe.LookupOrThrow(); + var module = universe.LookupOrThrow(); + var capturedBy = input.GetTarget(); + var isCaptured = (capturedBy != null); + // If another system has the mouse captured, don't do anything here. + if (isCaptured && (capturedBy != module)) return; + + var IO = ImGui.GetIO(); + + // Set the mouse position if ImGui wants to move it. + if (IO.WantSetMousePos) impl.Position = IO.MousePos; + + // Capture the mouse input it is above GUI elements. + if (IO.WantCaptureMouse != isCaptured) { + if (IO.WantCaptureMouse) input.Add (module); + else input.Remove(module); + } + + var cursor = ImGui.GetMouseCursor(); + impl.Cursor.CursorMode = (cursor == ImGuiMouseCursor.None) + ? CursorMode.Hidden : CursorMode.Normal; + // TODO: Use additional cursors once Silk.NET supports GLFW 3.4. + impl.Cursor.StandardCursor = cursor switch { + ImGuiMouseCursor.Arrow => StandardCursor.Arrow, + ImGuiMouseCursor.TextInput => StandardCursor.IBeam, + // ImGuiMouseCursor.ResizeAll => StandardCursor., + ImGuiMouseCursor.ResizeNS => StandardCursor.VResize, + ImGuiMouseCursor.ResizeEW => StandardCursor.HResize, + // ImGuiMouseCursor.ResizeNESW => StandardCursor., + // ImGuiMouseCursor.ResizeNWSE => StandardCursor., + ImGuiMouseCursor.Hand => StandardCursor.Hand, + // ImGuiMouseCursor.NotAllowed => StandardCursor., + _ => StandardCursor.Default, + }; + } + + [System] + public static void Update(TimeSpan delta, ImGuiData imgui) + => imgui.Controller.Update((float)delta.TotalSeconds); + + [System] + public static void ShowDemoWindow(ImGuiData _) + => ImGui.ShowDemoWindow(); + + [System] + public static void Render(ImGuiData imgui) + => imgui.Controller.Render(); +} diff --git a/src/gaemstone.Client/Systems/Input.cs b/src/gaemstone.Client/Systems/Input.cs deleted file mode 100644 index d2e418c..0000000 --- a/src/gaemstone.Client/Systems/Input.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using gaemstone.ECS; -using gaemstone.Flecs; -using Silk.NET.Input; -using Silk.NET.Maths; -using static gaemstone.Client.Systems.Windowing; - -namespace gaemstone.Client.Systems; - -[Module] -[DependsOn] -public class Input -{ - [Component] - public class RawInput - { - internal IInputContext? Context { get; set; } - - public Dictionary Keyboard { get; } = new(); - public Dictionary MouseButtons { get; } = new(); - public Vector2D MousePosition { get; set; } - public float MouseWheel { get; set; } - public float MouseWheelDelta { get; set; } - - public bool IsDown(Key key) => Keyboard.GetValueOrDefault(key)?.IsDown == true; - public bool IsDown(MouseButton button) => MouseButtons.GetValueOrDefault(button)?.IsDown == true; - } - - public class ButtonState - { - public TimeSpan TimePressed; - public bool IsDown; - public bool Pressed; - public bool Released; - } - - [System] - public static void ProcessInput(TimeSpan delta, - GameWindow window, RawInput input) - { - window.Handle.DoEvents(); - - input.Context ??= window.Handle.CreateInput(); - - foreach (var state in input.Keyboard.Values.Concat(input.MouseButtons.Values)) { - if (state.IsDown) state.TimePressed += delta; - state.Pressed = state.Released = false; - } - - var keyboard = input.Context.Keyboards[0]; - foreach (var key in keyboard.SupportedKeys) { - var state = input.Keyboard.GetValueOrDefault(key); - if (keyboard.IsKeyPressed(key)) { - if (state == null) input.Keyboard.Add(key, state = new()); - if (!state.IsDown) state.Pressed = true; - state.IsDown = true; - } else if (state != null) { - if (state.IsDown) state.Released = true; - state.IsDown = false; - } - } - - var mouse = input.Context.Mice[0]; - foreach (var button in mouse.SupportedButtons) { - var state = input.MouseButtons.GetValueOrDefault(button); - if (mouse.IsButtonPressed(button)) { - if (state == null) input.MouseButtons.Add(button, state = new()); - if (!state.IsDown) state.Pressed = true; - state.IsDown = true; - } else if (state != null) { - if (state.IsDown) state.Released = true; - state.IsDown = false; - } - } - - input.MousePosition = mouse.Position.ToGeneric(); - - input.MouseWheelDelta += mouse.ScrollWheels[0].Y - input.MouseWheel; - input.MouseWheel = mouse.ScrollWheels[0].Y; - } -} diff --git a/src/gaemstone.Client/Systems/InputManager.cs b/src/gaemstone.Client/Systems/InputManager.cs new file mode 100644 index 0000000..9ea0337 --- /dev/null +++ b/src/gaemstone.Client/Systems/InputManager.cs @@ -0,0 +1,141 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using gaemstone.ECS; +using gaemstone.Flecs; +using Silk.NET.Input; +using static gaemstone.Client.Components.InputComponents; +using static gaemstone.Client.Systems.Windowing; + +namespace gaemstone.Client.Systems; + +[Module] +[DependsOn] +[DependsOn] +public class InputManager +{ + [Component("InputContext"), Proxy] public struct ContextProxy { } + + [Component("MouseImpl" ), Proxy] public struct MouseProxy { } + [Component("KeyboardImpl"), Proxy] public struct KeyboardProxy { } + [Component("GamepadImpl" ), Proxy] public struct GamepadProxy { } + + [System] + public static void Initialize(Universe universe, + [Game] GameWindow window, [Source, Not] IInputContext _) + { + var input = universe.LookupOrThrow(); + var context = window.Handle.CreateInput(); + input.Set(context); + + // TODO: Add device names as documentation names to these entities. + + foreach (var impl in context.Mice.Take(1)) + input.LookupOrThrow("Mouse").Set(impl); + foreach (var impl in context.Keyboards.Take(1)) + input.LookupOrThrow("Keyboard").Set(impl); + foreach (var impl in context.Gamepads) + input.NewChild("Gamepad" + impl.Index).Add().Set(impl).Build(); + // TODO: Should we even support joysticks? + } + + + [Observer] + [Expression("CursorCapturedBy(Input, *)")] + public static void OnCursorCaptured(Universe universe) + => universe.LookupOrThrow().Get() + .Cursor.CursorMode = CursorMode.Raw; + + [Observer] + [Expression("CursorCapturedBy(Input, *)")] + public static void OnCursorReleased(Universe universe) + => universe.LookupOrThrow().Get() + .Cursor.CursorMode = CursorMode.Normal; + + + [System] + public static void ProcessMouse(TimeSpan delta, EntityRef mouse, IMouse impl) + { + var isCaptured = mouse.Parent!.Has(); + ref var position = ref mouse.NewChild("Position").Build().GetMut(); + ref var posDelta = ref mouse.NewChild("Delta" ).Build().GetMut(); + posDelta = impl.Position - position; + if (isCaptured) impl.Position = position; + else position = impl.Position; + + Update1D(delta, mouse.NewChild("Wheel").Build(), impl.ScrollWheels[0].Y); + + var buttons = mouse.NewChild("Buttons").Build(); + foreach (var button in impl.SupportedButtons) + Update1D(delta, buttons.NewChild(button.ToString()).Build(), + impl.IsButtonPressed(button) ? 1 : 0); + } + + [System] + public static void ProcessKeyboard(TimeSpan delta, EntityRef keyboard, IKeyboard impl) + { + foreach (var key in impl.SupportedKeys) { + var entity = keyboard.NewChild(key.ToString()).Build(); + Update1D(delta, entity, impl.IsKeyPressed(key) ? 1 : 0); + } + } + + [System] + public static void ProcessGamepad(TimeSpan delta, EntityRef gamepad, IGamepad impl) + { + var buttons = gamepad.NewChild("Buttons").Build(); + foreach (var button in impl.Buttons) + Update1D(delta, buttons.NewChild(button.Name.ToString()).Build(), button.Pressed ? 1 : 0); + foreach (var trigger in impl.Triggers) + Update1D(delta, gamepad.NewChild("Trigger" + trigger.Index).Build(), trigger.Position); + foreach (var thumbstick in impl.Thumbsticks) + Update2D(delta, gamepad.NewChild("Thumbstick" + thumbstick.Index).Build(), new(thumbstick.X, thumbstick.Y)); + } + + + private const float ActivationThreshold = 0.90F; + private const float DeactivationThreshold = 0.75F; + + private static void Update1D(TimeSpan delta, EntityRef entity, float current) + { + entity.GetMut() = current; + if (current >= ActivationThreshold) { + ref var active = ref entity.GetRefOrNull(); + if (Unsafe.IsNullRef(ref active)) { + entity.Set(new Active()); + entity.Add(); + } else active.Duration += delta; + } else if (current <= DeactivationThreshold) + entity.Remove(); + } + + private static void Update2D(TimeSpan delta, EntityRef entity, Vector2 current) + { + entity.GetMut() = current; + var magnitude = current.Length(); + if (magnitude >= ActivationThreshold) { + ref var active = ref entity.GetRefOrNull(); + if (Unsafe.IsNullRef(ref active)) { + entity.Set(new Active()); + entity.Add(); + } else active.Duration += delta; + } else if (magnitude <= DeactivationThreshold) + entity.Remove(); + } + + // TODO: flecs calls OnAdd observers repeatedly as the entity has other things added to it. + // So at least for now we need to add Activated manually, until this changes. + // [Observer] + // public static void OnActiveAdded(EntityRef entity, Active _) + // => entity.Add(); + + [Observer] + public static void OnActiveRemoved(EntityRef entity, Active _) + => entity.Add(); + + [System] + // [Expression("Activated || Deactivated")] + public static void ClearDeActivated(EntityRef entity, [Or] Activated _1, [Or] Deactivated _2) + => entity.Remove().Remove(); +} diff --git a/src/gaemstone.Client/Systems/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs index c088063..455d6c9 100644 --- a/src/gaemstone.Client/Systems/Renderer.cs +++ b/src/gaemstone.Client/Systems/Renderer.cs @@ -20,16 +20,16 @@ namespace gaemstone.Client.Systems; [DependsOn] [DependsOn] public class Renderer - : IModuleInitializer { private uint _program; private int _cameraMatrixUniform; private int _modelMatrixUniform; private Rule? _renderEntityRule; - public void Initialize(EntityRef module) + [Observer] + public void OnCanvasSet(Canvas canvas) { - var GL = module.Universe.LookupOrThrow().Get().GL; + var GL = canvas.GL; GL.Enable(EnableCap.DebugOutputSynchronous); GL.DebugMessageCallback(DebugCallback, 0); @@ -59,8 +59,8 @@ public class Renderer { var GL = canvas.GL; GL.UseProgram(_program); - GL.Viewport(default, canvas.Size); - GL.ClearColor(new Vector4D(0, 0, 0, 255)); + GL.Viewport(canvas.Size); + GL.ClearColor(Color.Black); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); } diff --git a/src/gaemstone.Client/Systems/TextureManager.cs b/src/gaemstone.Client/Systems/TextureManager.cs index c2b1bff..7a059aa 100644 --- a/src/gaemstone.Client/Systems/TextureManager.cs +++ b/src/gaemstone.Client/Systems/TextureManager.cs @@ -1,6 +1,7 @@ using System; using System.IO; using gaemstone.ECS; +using gaemstone.Flecs; using Silk.NET.OpenGL; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -15,12 +16,11 @@ namespace gaemstone.Client.Systems; [DependsOn] [DependsOn] public class TextureManager - : IModuleInitializer { - public void Initialize(EntityRef module) + [Observer] + public static void OnCanvasSet(Canvas canvas) { - var GL = module.Universe.LookupOrThrow().Get().GL; - + var GL = canvas.GL; // Upload single-pixel white texture into texture slot 0, so when // "no" texture is bound, we can still use the texture sampler. GL.BindTexture(TextureTarget.Texture2D, 0); diff --git a/src/gaemstone.Client/Systems/Windowing.cs b/src/gaemstone.Client/Systems/Windowing.cs index 03793ea..2782bb0 100644 --- a/src/gaemstone.Client/Systems/Windowing.cs +++ b/src/gaemstone.Client/Systems/Windowing.cs @@ -28,5 +28,8 @@ public class Windowing [System] public static void ProcessWindow(GameWindow window, Canvas canvas) - => canvas.Size = window.Handle.Size; + { + canvas.Size = window.Handle.Size; + window.Handle.DoEvents(); + } } diff --git a/src/gaemstone.Client/gaemstone.Client.csproj b/src/gaemstone.Client/gaemstone.Client.csproj index 1403d60..21d48ae 100644 --- a/src/gaemstone.Client/gaemstone.Client.csproj +++ b/src/gaemstone.Client/gaemstone.Client.csproj @@ -20,6 +20,7 @@ + diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index 44e0833..405ccf9 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -107,9 +107,9 @@ public static class SystemExtensions [UnmanagedCallersOnly] private static unsafe void Run(ecs_iter_t* flecsIter) { - // This is what flecs does, so I guess we'll do it too! var callback = CallbackContextHelper.Get((nint)flecsIter->binding_ctx); + // This is what flecs does, so I guess we'll do it too! var type = (&flecsIter->next == (delegate*)&ecs_query_next) ? IteratorType.Query : (IteratorType?)null; using var iter = new Iterator(callback.Universe, type, *flecsIter); diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/ECS/Universe+Modules.cs index e59b86e..d8b48e4 100644 --- a/src/gaemstone/ECS/Universe+Modules.cs +++ b/src/gaemstone/ECS/Universe+Modules.cs @@ -74,6 +74,8 @@ public class ModuleManager private void TryEnableModule(ModuleInfo module) { if (module.UnmetDependencies.Count > 0) return; + + Console.WriteLine($"Enabling module {module.Path} ..."); module.Enable(); // Find other modules that might be missing this module as a dependency. @@ -87,8 +89,6 @@ public class ModuleManager TryEnableModule(other); } - - Console.WriteLine("Enabled module " + module.Path); } public static EntityPath GetModulePath(Type type) @@ -102,7 +102,7 @@ public class ModuleManager // If global or path are specified in the attribute, return an absolute path. if (global || attr.Path != null) - return new(global, attr.Path ?? new[] { type.Name }); + return new(true, attr.Path ?? new[] { type.Name }); // Otherwise, create it based on the type's assembly, namespace and name. var assemblyName = type.Assembly.GetName().Name!; @@ -140,7 +140,7 @@ internal class ModuleInfo if (Type.GetConstructor(Type.EmptyTypes) == null) throw new Exception( $"Module {Type} must define public parameterless constructor"); - var module = Universe.New(path).Add(); + var module = Universe.New(Path).Add(); // Add module dependencies from [DependsOn<>] attributes. foreach (var dependsAttr in Type.GetMultiple().Where(attr => @@ -156,7 +156,7 @@ internal class ModuleInfo module.Add(dependency); } - Entity = module.Build().CreateLookup(type); + Entity = module.Build().CreateLookup(Type); } public void Enable()