diff --git a/.editorconfig b/.editorconfig index 4a9952c..bdda977 100644 --- a/.editorconfig +++ b/.editorconfig @@ -216,13 +216,13 @@ dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion -dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields -dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase +dotnet_naming_rule.private_fields_should_be_camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be_camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_camelcase.style = camelcase -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase +dotnet_naming_rule.private_static_fields_should_be_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_camelcase.style = camelcase dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields diff --git a/.github/workflows/dotnet-macos.yml b/.github/workflows/dotnet-macos.yml index 990c442..f5505c4 100644 --- a/.github/workflows/dotnet-macos.yml +++ b/.github/workflows/dotnet-macos.yml @@ -18,16 +18,20 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: workload install run: dotnet workload install maui - - name: Restore dependencies + - name: Restore dependencies - engine run: dotnet restore engine/Orbit.Engine/Orbit.Engine.csproj - - name: Restore dependencies + - name: Restore dependencies - input + run: dotnet restore engine/Orbit.Input/Orbit.Input.csproj + - name: Restore dependencies - tests run: dotnet restore engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj - name: Build engine - run: dotnet build engine/Orbit.Engine/Orbit.Engine.csproj --no-restore + run: dotnet build engine/Orbit.Engine/Orbit.Engine.csproj --no-restore -c Release + - name: Build input + run: dotnet build engine/Orbit.Input/Orbit.Input.csproj --no-restore -c Release - name: Build tests - run: dotnet build engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj --no-restore + run: dotnet build engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj --no-restore -c Release - name: Run tests run: dotnet test engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj --no-restore diff --git a/.github/workflows/dotnet-windows.yml b/.github/workflows/dotnet-windows.yml index 8c68107..a7e0d19 100644 --- a/.github/workflows/dotnet-windows.yml +++ b/.github/workflows/dotnet-windows.yml @@ -18,16 +18,20 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: workload install run: dotnet workload install maui - - name: Restore dependencies + - name: Restore dependencies - engine run: dotnet restore engine/Orbit.Engine/Orbit.Engine.csproj - - name: Restore dependencies + - name: Restore dependencies - input + run: dotnet restore engine/Orbit.Input/Orbit.Input.csproj + - name: Restore dependencies - tests run: dotnet restore engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj - name: Build engine - run: dotnet build engine/Orbit.Engine/Orbit.Engine.csproj --no-restore + run: dotnet build engine/Orbit.Engine/Orbit.Engine.csproj --no-restore -c Release + - name: Build input + run: dotnet build engine/Orbit.Input/Orbit.Input.csproj --no-restore -c Release - name: Build tests - run: dotnet build engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj --no-restore + run: dotnet build engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj --no-restore -c Release - name: Run tests run: dotnet test engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj --no-restore diff --git a/.github/workflows/nuget-release.yml b/.github/workflows/nuget-release.yml index d1daf9a..e2fd818 100644 --- a/.github/workflows/nuget-release.yml +++ b/.github/workflows/nuget-release.yml @@ -5,6 +5,10 @@ on: tags: - "v[0-9]+.[0-9]+.[0-9]+" - "v[0-9]+.[0-9]+.[0-9]+-preview[0-9]+" + - "engine[0-9]+.[0-9]+.[0-9]+" + - "engine[0-9]+.[0-9]+.[0-9]+-preview[0-9]+" + - "input[0-9]+.[0-9]+.[0-9]+" + - "input[0-9]+.[0-9]+.[0-9]+-preview[0-9]+" jobs: release-nuget: @@ -21,21 +25,9 @@ jobs: id: get_version uses: battila7/get-version-action@v2 - - name: Restore dependencies - run: dotnet restore engine/Orbit.Engine/Orbit.Engine.csproj - - - name: Build - run: dotnet build --configuration Release --no-restore engine/Orbit.Engine/Orbit.Engine.csproj /p:Version=${{ steps.get_version.outputs.version-without-v }} - - - name: Pack - run: dotnet pack engine/Orbit.Engine/Orbit.Engine.csproj -c Release /p:Version=${{ steps.get_version.outputs.version-without-v }} --no-build --output . - - - name: Push - run: dotnet nuget push Bijington.Orbit.Engine.${{ steps.get_version.outputs.version-without-v }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} - env: - GITHUB_TOKEN: ${{ secrets.NUGET_API_KEY }} - - - name: Push symbols - run: dotnet nuget push Bijington.Orbit.Engine.${{ steps.get_version.outputs.version-without-v }}.snupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} + - name: Package releases + shell: pwsh + run: | + .\scripts\package-releases.ps1 -Version ${{ steps.get_version.outputs.version-without-v }} -ApiKey ${{ secrets.NUGET_API_KEY }} env: GITHUB_TOKEN: ${{ secrets.NUGET_API_KEY }} \ No newline at end of file diff --git a/engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj b/engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj index c6b1ce2..44b3ada 100644 --- a/engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj +++ b/engine/Orbit.Engine.Tests/Orbit.Engine.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable true diff --git a/engine/Orbit.Engine/GameSceneManager.cs b/engine/Orbit.Engine/GameSceneManager.cs index 70d1ee9..5da7511 100644 --- a/engine/Orbit.Engine/GameSceneManager.cs +++ b/engine/Orbit.Engine/GameSceneManager.cs @@ -134,7 +134,7 @@ private void UpdateScene() var postUpdate = DateTime.UtcNow; var updateDuration = callbackMilliseconds - (postUpdate - currentUpdate).TotalMilliseconds; - var delayUntilNextUpdate = Math.Min(updateDuration, callbackMilliseconds); + var delayUntilNextUpdate = Math.Clamp(updateDuration, 0, callbackMilliseconds); dispatcher.DispatchDelayed( TimeSpan.FromMilliseconds(delayUntilNextUpdate), diff --git a/engine/Orbit.Engine/Orbit.Engine.csproj b/engine/Orbit.Engine/Orbit.Engine.csproj index 77ad11c..9551e6c 100644 --- a/engine/Orbit.Engine/Orbit.Engine.csproj +++ b/engine/Orbit.Engine/Orbit.Engine.csproj @@ -1,18 +1,18 @@ - net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst - $(TargetFrameworks);net8.0-windows10.0.19041.0 + net9.0;net9.0-android;net9.0-ios;net9.0-maccatalyst + $(TargetFrameworks);net9.0-windows10.0.19041.0 - + true true enable True - 14.2 - 14.0 + 15.0 + 15.0 21.0 10.0.19041.0 10.0.19041.0 @@ -58,10 +58,10 @@ - + false - + diff --git a/engine/Orbit.Input/GameControllers/ButtonValue.cs b/engine/Orbit.Input/GameControllers/ButtonValue.cs new file mode 100644 index 0000000..ea47fa6 --- /dev/null +++ b/engine/Orbit.Input/GameControllers/ButtonValue.cs @@ -0,0 +1,76 @@ +namespace Orbit.Input; + +/// +/// Represents a game controller button and its associated value. +/// +public abstract class ButtonValue +{ + /// + /// Creates a new instance of . + /// + /// The name of the component of a game controller that this button belongs to. + /// The name of the button. + protected ButtonValue(string parent, string name) + { + Name = NameHelper.GetName(parent, name); + } + + /// + /// Gets the name of the button. + /// + public string Name { get; } +} + +/// +public class ButtonValue : ButtonValue where TValue : struct +{ + private readonly GameController gameController; + private readonly IComparer comparer; + private TValue buttonValue; + + /// + /// Creates a new instance of . + /// + /// The that this button belongs to. + /// The name of the component of a game controller that this button belongs to. + /// The name of the button. + /// The to use in comparisons for value changed events. Particularly useful when dealing with values like where accuracy can be messy. + public ButtonValue(GameController gameController, string parent, string name, IComparer? comparer = null) + : base(parent, name) + { + this.gameController = gameController; + this.comparer = comparer ?? Comparer.Default; + } + + /// + /// Creates a new instance of . + /// + /// The that this button belongs to. + /// The name of the button. + /// The to use in comparisons for value changed events. Particularly useful when dealing with values like where accuracy can be messy. + public ButtonValue(GameController gameController, string name, IComparer? comparer = null) + : base(string.Empty, name) + { + this.gameController = gameController; + this.comparer = comparer ?? Comparer.Default; + } + + /// + /// Gets the current value for the button. + /// + public TValue Value + { + get => buttonValue; + internal set + { + if (comparer.Compare(buttonValue, value) == 0) + { + return; + } + + buttonValue = value; + + this.gameController.RaiseButtonValueChanged(this); + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/GameControllers/FloatComparer.cs b/engine/Orbit.Input/GameControllers/FloatComparer.cs new file mode 100644 index 0000000..f522d7c --- /dev/null +++ b/engine/Orbit.Input/GameControllers/FloatComparer.cs @@ -0,0 +1,31 @@ +namespace Orbit.Input; + +/// +/// A based implementation of . +/// +public class FloatComparer : IComparer +{ + private readonly float threshold; + + internal static FloatComparer Default { get; set; } = new FloatComparer(0.001f); + + /// + /// Creates a new instance of . + /// + /// The threshold to use when determining whether values are equal. + public FloatComparer(float threshold) + { + this.threshold = threshold; + } + + /// + public int Compare(float x, float y) + { + if (Math.Abs(x - y) < this.threshold) + { + return 0; + } + + return x < y ? 1 : -1; + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/GameControllers/GameController.cs b/engine/Orbit.Input/GameControllers/GameController.cs new file mode 100644 index 0000000..cb71c59 --- /dev/null +++ b/engine/Orbit.Input/GameControllers/GameController.cs @@ -0,0 +1,95 @@ +namespace Orbit.Input; + +/// +/// Represents a physical game controller that is connected to a device. +/// +public partial class GameController +{ + private readonly WeakEventManager weakEventManager = new(); + + /// + /// Gets the that represents the D-pad on the game controller. + /// + public Stick Dpad { get; } + + /// + /// Gets the that represents the left thumbstick on the game controller. + /// + public Stick LeftStick { get; } + + /// + /// Gets the that represents the right thumbstick on the game controller. + /// + public Stick RightStick { get; } + + /// + /// Gets the that represents the right most button on the controller. + /// Circle on Playstation and B on XBox controllers. + /// + public ButtonValue East { get; } + + /// + /// Gets the that represents the top most button on the controller. + /// Triangle on Playstation and Y on XBox controllers. + /// + public ButtonValue North { get; } + + /// + /// Gets the that represents the bottom most button on the controller. + /// X on Playstation and A on XBox controllers. + /// + public ButtonValue South { get; } + + /// + /// Gets the that represents the left most button on the controller. + /// Square on Playstation and X on XBox controllers. + /// + public ButtonValue West { get; } + + /// + /// Gets the that represents the left most button on the controller. + /// Options on Playstation and hamburger on XBox controllers. + /// + public ButtonValue Pause { get; } + + /// + /// Gets the that represents the left hand shoulder on the controller. + /// + public Shoulder LeftShoulder { get; } + + /// + /// Gets the that represents the right hand shoulder on the controller. + /// + public Shoulder RightShoulder { get; } + + /// + /// Event that is raised when a button on the game controller is detected as being pressed or released. + /// + public event EventHandler ButtonChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Event that is raised when a button that supports a varying value on the game controller is detected as being pressed or released to some degree. + /// + public event EventHandler ValueChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + internal void RaiseButtonValueChanged(ButtonValue buttonValue) + { + switch (buttonValue) + { + case ButtonValue floatValue: + weakEventManager.HandleEvent(this, new GameControllerValueChangedEventArgs(buttonValue.Name, floatValue.Value), nameof(ValueChanged)); + break; + case ButtonValue boolValue: + weakEventManager.HandleEvent(this, new GameControllerButtonChangedEventArgs(buttonValue.Name, boolValue.Value), nameof(ButtonChanged)); + break; + } + } +} diff --git a/engine/Orbit.Input/GameControllers/GameControllerButtonChangedEventArgs.cs b/engine/Orbit.Input/GameControllers/GameControllerButtonChangedEventArgs.cs new file mode 100644 index 0000000..91b5cf9 --- /dev/null +++ b/engine/Orbit.Input/GameControllers/GameControllerButtonChangedEventArgs.cs @@ -0,0 +1,28 @@ +namespace Orbit.Input; + +/// +/// Contains event data for all button pressed and released events. +/// +public class GameControllerButtonChangedEventArgs : EventArgs +{ + /// + /// Gets the name of the button. + /// + public string ButtonName { get; } + + /// + /// Gets whether the button is pressed or released. + /// + public bool IsPressed { get; } + + /// + /// Creates a new instance of . + /// + /// The name of the button. + /// Whether the button is pressed or released. + public GameControllerButtonChangedEventArgs(string buttonName, bool isPressed) + { + ButtonName = buttonName; + IsPressed = isPressed; + } +} diff --git a/engine/Orbit.Input/GameControllers/GameControllerConnectedEventArgs.cs b/engine/Orbit.Input/GameControllers/GameControllerConnectedEventArgs.cs new file mode 100644 index 0000000..da3766c --- /dev/null +++ b/engine/Orbit.Input/GameControllers/GameControllerConnectedEventArgs.cs @@ -0,0 +1,21 @@ +namespace Orbit.Input; + +/// +/// Contains event data for when game controllers are detected as being connected to the device. +/// +public class GameControllerConnectedEventArgs : EventArgs +{ + /// + /// Creates a new instance of . + /// + /// The that has been detected and being connected to the device. + public GameControllerConnectedEventArgs(GameController gameController) + { + GameController = gameController; + } + + /// + /// Gets the that has been detected and being connected to the device. + /// + public GameController GameController { get; } +} diff --git a/engine/Orbit.Input/GameControllers/GameControllerManager.cs b/engine/Orbit.Input/GameControllers/GameControllerManager.cs new file mode 100644 index 0000000..6973337 --- /dev/null +++ b/engine/Orbit.Input/GameControllers/GameControllerManager.cs @@ -0,0 +1,43 @@ +namespace Orbit.Input; + +/// +/// Provides the ability to interact with game controller support on each platform. +/// +public partial class GameControllerManager +{ + private readonly List gameControllers = []; + private readonly WeakEventManager weakEventManager = new(); + + private static GameControllerManager? current; + + /// + /// Gets the current instance of the . + /// + public static GameControllerManager Current => current ??= new GameControllerManager(); + + /// + /// Starts the controller discovery process. Make sure to subscribe to the event in order to be notified when a controller has been discovered. + /// + /// + public partial Task StartDiscovery(); + + /// + /// Gets the list of s that are connected to the device. + /// + public IReadOnlyCollection GameControllers => gameControllers; + + /// + /// Event that is raised when a is detected as being connected to the device. + /// + public event EventHandler GameControllerConnected + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + private void OnGameControllerConnected(GameController controller) + { + gameControllers.Add(controller); + weakEventManager.HandleEvent(this, new GameControllerConnectedEventArgs(controller), nameof(GameControllerConnected)); + } +} diff --git a/engine/Orbit.Input/GameControllers/GameControllerOptions.cs b/engine/Orbit.Input/GameControllers/GameControllerOptions.cs new file mode 100644 index 0000000..45d3375 --- /dev/null +++ b/engine/Orbit.Input/GameControllers/GameControllerOptions.cs @@ -0,0 +1,12 @@ +namespace Orbit.Input; + +/// +/// Defines the game controller specific options used to override specific functionality. +/// +public partial class GameControllerOptions +{ + /// + /// Gets the threshold to use when comparing floats together in order to handle the inaccuracies that come with floats. + /// + public float ComparisonThreshold { get; set; } = 0.001f; +} \ No newline at end of file diff --git a/engine/Orbit.Input/GameControllers/GameControllerValueChangedEventArgs.cs b/engine/Orbit.Input/GameControllers/GameControllerValueChangedEventArgs.cs new file mode 100644 index 0000000..88d837e --- /dev/null +++ b/engine/Orbit.Input/GameControllers/GameControllerValueChangedEventArgs.cs @@ -0,0 +1,28 @@ +namespace Orbit.Input; + +/// +/// Contains event data for all button value changed events. +/// +public class GameControllerValueChangedEventArgs : EventArgs +{ + /// + /// Gets the name of the button. + /// + public string ButtonName { get; } + + /// + /// Gets how much the button has been pressed. + /// + public float Value { get; } + + /// + /// Creates a new instance of . + /// + /// The name of the button. + /// How much the button has been pressed. + public GameControllerValueChangedEventArgs(string buttonName, float value) + { + ButtonName = buttonName; + Value = value; + } +} diff --git a/engine/Orbit.Input/GameControllers/Shoulder.cs b/engine/Orbit.Input/GameControllers/Shoulder.cs new file mode 100644 index 0000000..074b882 --- /dev/null +++ b/engine/Orbit.Input/GameControllers/Shoulder.cs @@ -0,0 +1,30 @@ +namespace Orbit.Input; + +/// +/// Represents a collection of buttons on ther 'shoulder' of a game controller. +/// +public class Shoulder +{ + /// + /// Creates a new instance of . + /// + /// The that the shoulder belongs to. + /// The name of the shoulder (e.g. left or right). + public Shoulder(GameController controller, string name) + { + Button = new ButtonValue(controller, name, nameof(Button)); + Trigger = new ButtonValue(controller, name, nameof(Trigger), FloatComparer.Default); + } + + /// + /// Gets the that represents the top button on the shoulder. + /// This is represented as a simple pressed/released button. + /// + public ButtonValue Button { get; } + + /// + /// Gets the that represents the bottom button on the shoulder. + /// This is represented as a value between 0 and 1 based on how much the button is pressed. + /// + public ButtonValue Trigger { get; } +} diff --git a/engine/Orbit.Input/GameControllers/Stick.cs b/engine/Orbit.Input/GameControllers/Stick.cs new file mode 100644 index 0000000..4c78e4f --- /dev/null +++ b/engine/Orbit.Input/GameControllers/Stick.cs @@ -0,0 +1,30 @@ +namespace Orbit.Input; + +/// +/// Definition of a multi-axis component on a game pad. This could be a joystick, thumbstick or d-pad. +/// +public class Stick +{ + /// + /// Creates a new instance of . + /// + /// The that the stick belongs to. + /// The name of the stick (e.g. left or right). + public Stick(GameController controller, string name) + { + XAxis = new ButtonValue(controller, name, nameof(XAxis), FloatComparer.Default); + YAxis = new ButtonValue(controller, name, nameof(YAxis), FloatComparer.Default); + } + + /// + /// Gets the x-axis value. + /// This is represented as a value between -1 (fully pressed to the left) and 1 (fully pressed to the right). + /// + public ButtonValue XAxis { get; } + + /// + /// Gets the y-axis value. + /// This is represented as a value between -1 (fully pressed to the top) and 1 (fully pressed to the bottom). + /// + public ButtonValue YAxis { get; } +} diff --git a/engine/Orbit.Input/Keyboards/KeyboardKey.cs b/engine/Orbit.Input/Keyboards/KeyboardKey.cs new file mode 100644 index 0000000..f113638 --- /dev/null +++ b/engine/Orbit.Input/Keyboards/KeyboardKey.cs @@ -0,0 +1,121 @@ +namespace Orbit.Input; + +#pragma warning disable CS1591 // Let's be pragmatic here... most of these are self explanatory. +/// +/// A platform-independent enumeration of keyboard keys. +/// +public enum KeyboardKey +{ + Unknown, + Backspace, + Tab, + Enter, + ShiftLeft, + ShiftRight, + ControlLeft, + ControlRight, + AltLeft, + AltRight, + Pause, + CapsLock, + Escape, + Space, + PageUp, + PageDown, + End, + Home, + IntBackslash, + ArrowLeft, + ArrowUp, + ArrowRight, + ArrowDown, + PrintScreen, + Insert, + Delete, + Digit0, + Digit1, + Digit2, + Digit3, + Digit4, + Digit5, + Digit6, + Digit7, + Digit8, + Digit9, + KeyA, + KeyB, + KeyC, + KeyD, + KeyE, + KeyF, + KeyG, + KeyH, + KeyI, + KeyJ, + KeyK, + KeyL, + KeyM, + KeyN, + KeyO, + KeyP, + KeyQ, + KeyR, + KeyS, + KeyT, + KeyU, + KeyV, + KeyW, + KeyX, + KeyY, + KeyZ, + MetaLeft, + MetaRight, + ContextMenu, + Numpad0, + Numpad1, + Numpad2, + Numpad3, + Numpad4, + Numpad5, + Numpad6, + Numpad7, + Numpad8, + Numpad9, + NumpadMultiply, + NumpadAdd, + NumpadSubtract, + NumpadDecimal, + NumpadDivide, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + NumLock, + ScrollLock, + AudioVolumeMute, + AudioVolumeDown, + AudioVolumeUp, + LaunchMediaPlayer, + LaunchApplication1, + LaunchApplication2, + Semicolon, + Equal, + Comma, + Minus, + Period, + Slash, + Backquote, + BracketLeft, + Backslash, + BracketRight, + Quote, +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/engine/Orbit.Input/Keyboards/KeyboardKeyChangeEventArgs.cs b/engine/Orbit.Input/Keyboards/KeyboardKeyChangeEventArgs.cs new file mode 100644 index 0000000..c151939 --- /dev/null +++ b/engine/Orbit.Input/Keyboards/KeyboardKeyChangeEventArgs.cs @@ -0,0 +1,21 @@ +namespace Orbit.Input; + +/// +/// Contains event data for all key pressed and released events. +/// +public class KeyboardKeyChangeEventArgs : EventArgs +{ + /// + /// Gets the that triggered the event. + /// + public KeyboardKey Key { get; } + + /// + /// Creates a new instance of + /// + /// The that triggered the event. + public KeyboardKeyChangeEventArgs(KeyboardKey key) + { + Key = key; + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Keyboards/KeyboardManager.cs b/engine/Orbit.Input/Keyboards/KeyboardManager.cs new file mode 100644 index 0000000..da8dab5 --- /dev/null +++ b/engine/Orbit.Input/Keyboards/KeyboardManager.cs @@ -0,0 +1,98 @@ +using System.Collections.Concurrent; + +namespace Orbit.Input; + +/// +/// Provides the ability to interact with a keyboard connected to a device. +/// +public partial class KeyboardManager +{ + private static KeyboardManager? current; + private readonly ConcurrentDictionary pressedKeys; + private readonly IReadOnlyDictionary modifierKeys; + private readonly WeakEventManager weakEventManager = new(); + + private KeyboardManager() + { + pressedKeys = new ConcurrentDictionary( + Enum.GetValues(typeof(KeyboardKey)).Cast().ToDictionary(k => k, k => false)); + + modifierKeys = new Dictionary + { + { KeyboardKey.ShiftLeft, KeyboardModifiers.ShiftLeft }, + { KeyboardKey.ShiftRight, KeyboardModifiers.ShiftRight }, + { KeyboardKey.AltLeft, KeyboardModifiers.AltLeft }, + { KeyboardKey.AltRight, KeyboardModifiers.AltRight }, + { KeyboardKey.ControlLeft, KeyboardModifiers.ControlLeft }, + { KeyboardKey.ControlRight, KeyboardModifiers.ControlRight }, + }; + } + + /// + /// Provides the ability to check the state of the supplied . + /// + /// The to check the pressed state for. + public bool this[KeyboardKey key] => pressedKeys.TryGetValue(key, out var value) && value; + + /// + /// Gets the current instance of the . + /// + public static KeyboardManager Current => current ??= new KeyboardManager(); + + /// + /// Gets whether any are currently pressed. + /// + public KeyboardModifiers Modifiers { get; private set; } + + /// + /// Event that is raised when a is detected as being pressed. + /// + public event EventHandler KeyDown + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Event that is raised when a is detected as being released. + /// + public event EventHandler KeyUp + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + private void ApplyModifier(KeyboardKey key) + { + if (modifierKeys.TryGetValue(key, out var modifiers)) + { + Modifiers |= modifiers; + } + } + + private void ClearModifier(KeyboardKey key) + { + if (modifierKeys.TryGetValue(key, out var modifiers)) + { + Modifiers ^= modifiers; + } + } + + private void KeyboardKeyPressed(KeyboardKey key) + { + ApplyModifier(key); + + pressedKeys.TryUpdate(key, true, false); + + weakEventManager.HandleEvent(this, new KeyboardKeyChangeEventArgs(key), nameof(KeyDown)); + } + + private void KeyboardKeyReleased(KeyboardKey key) + { + ClearModifier(key); + + pressedKeys.TryUpdate(key, false, true); + + weakEventManager.HandleEvent(this, new KeyboardKeyChangeEventArgs(key), nameof(KeyUp)); + } +} diff --git a/engine/Orbit.Input/Keyboards/KeyboardModifiers.cs b/engine/Orbit.Input/Keyboards/KeyboardModifiers.cs new file mode 100644 index 0000000..511ba33 --- /dev/null +++ b/engine/Orbit.Input/Keyboards/KeyboardModifiers.cs @@ -0,0 +1,27 @@ +namespace Orbit.Input; + +#pragma warning disable CS1591 // Let's be pragmatic here... most of these are self explanatory. +[Flags] +public enum KeyboardModifiers +{ + None = 0, + + ShiftLeft = 1, + + ShiftRight = 2, + + Shift = ShiftLeft | ShiftRight, + + AltLeft = 4, + + AltRight = 8, + + Alt = AltLeft | AltRight, + + ControlLeft = 16, + + ControlRight = 32, + + Control = ControlLeft | ControlRight +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/engine/Orbit.Input/Keyboards/KeyboardOptions.cs b/engine/Orbit.Input/Keyboards/KeyboardOptions.cs new file mode 100644 index 0000000..4d32ee7 --- /dev/null +++ b/engine/Orbit.Input/Keyboards/KeyboardOptions.cs @@ -0,0 +1,8 @@ +namespace Orbit.Input; + +/// +/// Defines the keyboard specific options used to override specific functionality. +/// +public partial class KeyboardOptions +{ +} \ No newline at end of file diff --git a/engine/Orbit.Input/MauiAppBuilderExtensions.cs b/engine/Orbit.Input/MauiAppBuilderExtensions.cs new file mode 100644 index 0000000..f3696ec --- /dev/null +++ b/engine/Orbit.Input/MauiAppBuilderExtensions.cs @@ -0,0 +1,175 @@ +using Microsoft.Maui.LifecycleEvents; + +namespace Orbit.Input; + +/// +/// Extensions for the . +/// +public static class MauiAppBuilderExtensions +{ + public static MauiAppBuilder UseOrbitInput( + this MauiAppBuilder builder, + Action? configureControllerOptions = null, + Action? configureKeyboardOptions = null) => + builder + .UseOrbitGameController(configureControllerOptions) + .UseOrbitKeyboard(configureKeyboardOptions); + + /// + /// Initializes the game controller libraries to be integrated with the current application. + /// + /// The to register the package with. + /// + /// The mechanism to define any options to customize the game controller integration. Note this is optional. + /// An example of configuring the integration to not auto attach on Android is as follows: + /// + /// builder + /// .UseOrbitGameController( + /// configureControllerOptions: options => + /// { + ///#if ANDROID + /// options.AutoAttachToLifecycleEvents = false; + ///#endif + /// }); + /// + /// + /// The supplied in order to allow for chaining of method calls. + public static MauiAppBuilder UseOrbitGameController( + this MauiAppBuilder builder, + Action? configureControllerOptions) + { + var controllerOptions = new GameControllerOptions(); + configureControllerOptions?.Invoke(controllerOptions); + + FloatComparer.Default = new FloatComparer(controllerOptions.ComparisonThreshold); + +// builder.ConfigureMauiHandlers(handlers => +// { +// #if IOS || MACCATALYST +// PageHandler.PlatformViewFactory = (handler) => +// { +// var vc = new KeyboardPageViewController(handler.VirtualView, handler.MauiContext!); +// handler.ViewController = vc; +// return (Microsoft.Maui.Platform.ContentView)vc.View!.Subviews[0]; +// }; +// #endif +// }); + + builder.ConfigureLifecycleEvents(appLifecycle => + { +#if ANDROID + appLifecycle.AddAndroid(android => + { + bool appCreated = false; + + android.OnCreate((activity, bundle) => + { + if (appCreated) + { + return; + } + + appCreated = true; + + if (controllerOptions.AutoAttachToLifecycleEvents) + { + GameControllerManager.Current.AttachToCurrentActivity(activity); + } + }); + }); +#endif + }); + +#if WINDOWS + GameControllerManager.Current.StartControllerMonitoringUponDetection = controllerOptions.StartControllerMonitoringUponDetection; + GameControllerManager.Current.ControllerUpdateFrequency = controllerOptions.ControllerUpdateFrequency; +#endif + + return builder; + } + + /// + /// Initializes the keyboard libraries to be integrated with the current application. + /// + /// The to register the package with. + /// + /// The mechanism to define any options to customize the keyboard integration. Note this is optional. + /// An example of configuring the integration to not auto attach on Android is as follows: + /// + /// builder + /// .UseOrbitGameController( + /// configureKeyboardOptions: options => + /// { + ///#if ANDROID + /// options.AutoAttachToLifecycleEvents = false; + ///#endif + /// }); + /// + /// + /// The supplied in order to allow for chaining of method calls. + public static MauiAppBuilder UseOrbitKeyboard( + this MauiAppBuilder builder, + Action? configureKeyboardOptions = null) + { + var keyboardOptions = new KeyboardOptions(); + configureKeyboardOptions?.Invoke(keyboardOptions); + +// builder.ConfigureMauiHandlers(handlers => +// { +// #if IOS || MACCATALYST +// PageHandler.PlatformViewFactory = (handler) => +// { +// var vc = new KeyboardPageViewController(handler.VirtualView, handler.MauiContext!); +// handler.ViewController = vc; +// return (Microsoft.Maui.Platform.ContentView)vc.View!.Subviews[0]; +// }; +// #endif +// }); + + builder.ConfigureLifecycleEvents(appLifecycle => + { +#if ANDROID + appLifecycle.AddAndroid(android => + { + bool appCreated = false; + + android.OnCreate((activity, _) => + { + if (appCreated) + { + return; + } + + appCreated = true; + + if (keyboardOptions.AutoAttachToLifecycleEvents) + { + KeyboardManager.Current.AttachToCurrentActivity(activity); + } + }); + }); +#elif WINDOWS + appLifecycle.AddEvent( + "OnLaunched", + (Microsoft.UI.Xaml.Application application, Microsoft.UI.Xaml.LaunchActivatedEventArgs args) => + { + var appWindow = Application.Current?.Windows.First(); + + if (appWindow is null) + { + return; + } + + var window = appWindow.Handler?.PlatformView as Microsoft.Maui.MauiWinUIWindow; + + if (window is not null) + { + KeyboardManager.Current.AttachKeyboard(window.Content); + } + }); +#endif + }); + + return builder; + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/NameHelper.cs b/engine/Orbit.Input/NameHelper.cs new file mode 100644 index 0000000..1835eb5 --- /dev/null +++ b/engine/Orbit.Input/NameHelper.cs @@ -0,0 +1,14 @@ +namespace Orbit.Input; + +internal static class NameHelper +{ + internal static string GetName(string parent, string child) + { + if (string.IsNullOrEmpty(parent)) + { + return child; + } + + return parent + "." + child; + } +} diff --git a/engine/Orbit.Input/Orbit.Input.csproj b/engine/Orbit.Input/Orbit.Input.csproj new file mode 100644 index 0000000..cf00ece --- /dev/null +++ b/engine/Orbit.Input/Orbit.Input.csproj @@ -0,0 +1,59 @@ + + + + net9.0-android;net9.0-ios;net9.0-maccatalyst + $(TargetFrameworks);net9.0-windows10.0.19041.0 + + + true + true + enable + enable + + + + 15.0 + 15.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + Orbit.Input + + + + Shaun Lawrence + The Orbit input library provides support for game controller input. + + .NET MAUI, Game Engine, 2D Game + + Bijington.Orbit.Input + + https://github.com/bijington/orbit + https://github.com/bijington/orbit + Orbit Input + Copyright(c) 2025 Shaun Lawrence + + MIT + true + true + true + snupkg + true + true + Release;Debug + + + + + + + + + true + / + readme.md + + + + diff --git a/engine/Orbit.Input/Platforms/Android/GameController.cs b/engine/Orbit.Input/Platforms/Android/GameController.cs new file mode 100644 index 0000000..a82b34b --- /dev/null +++ b/engine/Orbit.Input/Platforms/Android/GameController.cs @@ -0,0 +1,206 @@ +using Android.Views; + +namespace Orbit.Input; + +// All the code in this file is only included on Android. +public partial class GameController +{ + private readonly int deviceId; + + public GameController(int deviceId) + { + this.deviceId = deviceId; + + Dpad = new Stick(this, nameof(Dpad)); + LeftStick = new Stick(this, nameof(LeftStick)); + RightStick = new Stick(this, nameof(RightStick)); + + LeftShoulder = new Shoulder(this, nameof(LeftShoulder)); + RightShoulder = new Shoulder(this, nameof(RightShoulder)); + + North = new ButtonValue(this, nameof(North)); + South = new ButtonValue(this, nameof(South)); + East = new ButtonValue(this, nameof(East)); + West = new ButtonValue(this, nameof(West)); + Pause = new ButtonValue(this, nameof(Pause)); + } + + public bool OnGenericMotionEvent(MotionEvent motionEvent) + { + if (motionEvent.DeviceId != this.deviceId) + { + return false; + } + + // Check that the event came from a game controller + if (motionEvent.Source.HasFlag(InputSourceType.Joystick) && + motionEvent.Action == MotionEventActions.Move) + { + // Process all historical movement samples in the batch + var historySize = motionEvent.HistorySize; + + // Process the movements starting from the + // earliest historical position in the batch + for (var i = 0; i < historySize; i++) + { + // Process the event at historical position i + ProcessJoystickInput(motionEvent, i); + } + + // Process the current movement sample in the batch (position -1) + ProcessJoystickInput(motionEvent, -1); + } + + return true; + } + + public bool OnKeyDown(InputEvent inputEvent) + { + if (inputEvent.DeviceId != this.deviceId) + { + return false; + } + + if (!IsDpadDevice(inputEvent)) + { + return false; + } + + switch (inputEvent) + { + // If the input event is a MotionEvent, check its hat axis values. + case MotionEvent motionEvent: + // Use the hat axis value to find the D-pad direction + Dpad.XAxis.Value = motionEvent.GetAxisValue(Axis.HatX); + Dpad.YAxis.Value = motionEvent.GetAxisValue(Axis.HatY); + break; + + // If the input event is a KeyEvent, check its key code. + // Use the key code to find the D-pad direction. + case KeyEvent { KeyCode: Keycode.DpadLeft }: + Dpad.XAxis.Value = -1; + break; + + case KeyEvent { KeyCode: Keycode.DpadRight }: + Dpad.XAxis.Value = 1; + break; + + case KeyEvent { KeyCode: Keycode.DpadDown }: + Dpad.YAxis.Value = -1; + break; + + case KeyEvent { KeyCode: Keycode.DpadUp }: + Dpad.YAxis.Value = 1; + break; + + case KeyEvent { KeyCode: Keycode.ButtonL1 }: + LeftShoulder.Button.Value = true; + break; + + case KeyEvent { KeyCode: Keycode.ButtonR1 }: + RightShoulder.Button.Value = true; + break; + } + + return true; + } + + public bool OnKeyUp(InputEvent inputEvent) + { + if (inputEvent.DeviceId != this.deviceId) + { + return false; + } + + if (!IsDpadDevice(inputEvent)) + { + return false; + } + + switch (inputEvent) + { + // If the input event is a MotionEvent, check its hat axis values. + case MotionEvent: + // Use the hat axis value to find the D-pad direction + Dpad.XAxis.Value = 0; + Dpad.YAxis.Value = 0; + break; + + // If the input event is a KeyEvent, check its key code. + // Use the key code to find the D-pad direction. + case KeyEvent { KeyCode: Keycode.DpadLeft }: + Dpad.XAxis.Value = 0; + break; + + case KeyEvent { KeyCode: Keycode.DpadRight }: + Dpad.XAxis.Value = 0; + break; + + case KeyEvent { KeyCode: Keycode.DpadDown }: + Dpad.YAxis.Value = 0; + break; + + case KeyEvent { KeyCode: Keycode.DpadUp }: + Dpad.YAxis.Value = 0; + break; + + case KeyEvent { KeyCode: Keycode.ButtonL1 }: + LeftShoulder.Button.Value = false; + break; + + case KeyEvent { KeyCode: Keycode.ButtonR1 }: + RightShoulder.Button.Value = false; + break; + } + + return true; + } + + private static float GetCenteredAxis(MotionEvent motionEvent, InputDevice device, Axis axis, int historyPos) + { + var range = device.GetMotionRange(axis, motionEvent.Source); + + // A joystick at rest does not always report an absolute position of + // (0,0). Use the getFlat() method to determine the range of values + // bounding the joystick axis center. + if (range is not null) + { + var flat = range.Flat; + var value = historyPos < 0 ? motionEvent.GetAxisValue(axis): + motionEvent.GetHistoricalAxisValue(axis, historyPos); + + // Ignore axis values that are within the 'flat' region of the + // joystick axis center. + if (Math.Abs(value) > flat) + { + return value; + } + } + return 0; + } + + private static bool IsDpadDevice(InputEvent inputEvent) + { + // Check that input comes from a device with directional pads. + return inputEvent.Source.HasFlag(InputSourceType.Dpad); + } + + private void ProcessJoystickInput(MotionEvent motionEvent, int historyPos) + { + var inputDevice = motionEvent.Device; + + if (inputDevice is null) + { + return; + } + + LeftStick.XAxis.Value = GetCenteredAxis(motionEvent, inputDevice, Axis.X, historyPos); + LeftStick.YAxis.Value = GetCenteredAxis(motionEvent, inputDevice, Axis.Y, historyPos); + + RightStick.XAxis.Value = GetCenteredAxis(motionEvent, inputDevice, Axis.Z, historyPos); + RightStick.YAxis.Value = GetCenteredAxis(motionEvent, inputDevice, Axis.Rz, historyPos); + + LeftShoulder.Trigger.Value = GetCenteredAxis(motionEvent, inputDevice, Axis.Ltrigger, historyPos); + RightShoulder.Trigger.Value = GetCenteredAxis(motionEvent, inputDevice, Axis.Rtrigger, historyPos); + } +} diff --git a/engine/Orbit.Input/Platforms/Android/GameControllerManager.cs b/engine/Orbit.Input/Platforms/Android/GameControllerManager.cs new file mode 100644 index 0000000..e57b70a --- /dev/null +++ b/engine/Orbit.Input/Platforms/Android/GameControllerManager.cs @@ -0,0 +1,75 @@ +using Android.App; +using Android.Views; + +namespace Orbit.Input; + +public partial class GameControllerManager +{ + private KeyListener? keyListener; + private GenericMotionListener? genericMotionListener; + + private GameControllerManager() + { + } + + public void AttachToCurrentActivity(Activity activity) + { + keyListener = new KeyListener(activity, (code, e) => + { + switch (e.Action) + { + case KeyEventActions.Down when OnKeyDown(code, e): + case KeyEventActions.Up when OnKeyUp(code, e): + return true; + } + + return false; + }); + + genericMotionListener = new GenericMotionListener(activity, OnGenericMotionEvent); + } + + public partial Task StartDiscovery() + { + var deviceIds = InputDevice.GetDeviceIds(); + + if (deviceIds is null) + { + return Task.CompletedTask; + } + + foreach (var deviceId in deviceIds) + { + var device = InputDevice.GetDevice(deviceId); + + if (device is null) + { + continue; + } + + var sources = device.Sources; + + if (sources.HasFlag(InputSourceType.Gamepad) || sources.HasFlag(InputSourceType.Joystick)) + { + OnGameControllerConnected(new GameController(deviceId)); + } + } + + return Task.CompletedTask; + } + + public bool OnGenericMotionEvent(MotionEvent? e) + { + return e is not null && gameControllers.Any(controller => controller.OnGenericMotionEvent(e)); + } + + public bool OnKeyDown(Keycode keyCode, KeyEvent? e) + { + return e is not null && gameControllers.Any(controller => controller.OnKeyDown(e)); + } + + public bool OnKeyUp(Keycode keyCode, KeyEvent? e) + { + return e is not null && gameControllers.Any(controller => controller.OnKeyUp(e)); + } +} diff --git a/engine/Orbit.Input/Platforms/Android/GameControllerOptions.cs b/engine/Orbit.Input/Platforms/Android/GameControllerOptions.cs new file mode 100644 index 0000000..cbffabb --- /dev/null +++ b/engine/Orbit.Input/Platforms/Android/GameControllerOptions.cs @@ -0,0 +1,6 @@ +namespace Orbit.Input; + +public partial class GameControllerOptions +{ + public bool AutoAttachToLifecycleEvents { get; set; } = true; +} diff --git a/engine/Orbit.Input/Platforms/Android/GenericMotionListener.cs b/engine/Orbit.Input/Platforms/Android/GenericMotionListener.cs new file mode 100644 index 0000000..3d6878d --- /dev/null +++ b/engine/Orbit.Input/Platforms/Android/GenericMotionListener.cs @@ -0,0 +1,31 @@ +using Android.App; +using Android.Views; + +using View = Android.Views.View; + +namespace Orbit.Input; + +public class GenericMotionListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalFocusChangeListener, View.IOnGenericMotionListener +{ + private readonly Func callback; + + public GenericMotionListener(Activity activity, Func callback) + { + this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); + + activity.Window?.DecorView.ViewTreeObserver?.AddOnGlobalFocusChangeListener(this); + } + + public void OnGlobalFocusChanged(View? oldFocus, View? newFocus) + { + oldFocus?.SetOnGenericMotionListener(null); + + newFocus?.SetOnGenericMotionListener(this); + } + + public bool OnGenericMotion(View? v, MotionEvent? e) + { + // Only return if something handles it + return e is not null && callback.Invoke(e); + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/Android/KeyListener.cs b/engine/Orbit.Input/Platforms/Android/KeyListener.cs new file mode 100644 index 0000000..5b87d41 --- /dev/null +++ b/engine/Orbit.Input/Platforms/Android/KeyListener.cs @@ -0,0 +1,38 @@ +using Android.App; +using Android.Views; + +using View = Android.Views.View; + +namespace Orbit.Input; + +public class KeyListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalFocusChangeListener, View.IOnKeyListener +{ + private readonly Func callback; + + public KeyListener(Activity activity, Func callback) + { + this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); + + activity.Window?.DecorView.ViewTreeObserver?.AddOnGlobalFocusChangeListener(this); + } + + public void OnGlobalFocusChanged(View? oldFocus, View? newFocus) + { + oldFocus?.SetOnKeyListener(null); + + newFocus?.SetOnKeyListener(this); + } + + /// + /// You have to return `true` if the key was handled. We will return `true` always in this implementation. + /// + /// + /// + /// + /// + public bool OnKey(View? v, Keycode keyCode, KeyEvent? e) + { + // Only return if something handles it + return e is not null && callback.Invoke(keyCode, e); + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/Android/KeyboardManager.cs b/engine/Orbit.Input/Platforms/Android/KeyboardManager.cs new file mode 100644 index 0000000..26bd78d --- /dev/null +++ b/engine/Orbit.Input/Platforms/Android/KeyboardManager.cs @@ -0,0 +1,151 @@ +using Android.InputMethodServices; +using Android.Views; +using Activity = Android.App.Activity; +using Keycode = Android.Views.Keycode; +using View = Android.Views.View; + +namespace Orbit.Input; + +public partial class KeyboardManager +{ + private KeyListener? listener; + + private static bool IsKeyboardDevice(InputEvent inputEvent) + { + // Check that input comes from a device with directional pads. + return inputEvent.Source.HasFlag(InputSourceType.Keyboard); + } + + public void AttachToCurrentActivity(Activity activity) + { + listener = new(activity, (code, e) => + { + if (!IsKeyboardDevice(e)) + { + return false; + } + + var mapped = MapToMaui(code); + + switch (e) + { + case { Action: KeyEventActions.Down }: + KeyboardKeyPressed(mapped); + break; + case { Action: KeyEventActions.Up }: + KeyboardKeyReleased(mapped); + break; + } + + return true; + }); + } + + public static KeyboardKey MapToMaui(Keycode keycode) + { + switch (keycode) + { + case Keycode.Space: return KeyboardKey.Space; + case Keycode.DpadLeft: return KeyboardKey.ArrowLeft; + case Keycode.DpadUp: return KeyboardKey.ArrowUp; + case Keycode.DpadRight: return KeyboardKey.ArrowRight; + case Keycode.DpadDown: return KeyboardKey.ArrowDown; + + case Keycode.Num0: return KeyboardKey.Digit0; + case Keycode.Num1: return KeyboardKey.Digit1; + case Keycode.Num2: return KeyboardKey.Digit2; + case Keycode.Num3: return KeyboardKey.Digit3; + case Keycode.Num4: return KeyboardKey.Digit4; + case Keycode.Num5: return KeyboardKey.Digit5; + case Keycode.Num6: return KeyboardKey.Digit6; + case Keycode.Num7: return KeyboardKey.Digit7; + case Keycode.Num8: return KeyboardKey.Digit8; + case Keycode.Num9: return KeyboardKey.Digit9; + + case Keycode.A: return KeyboardKey.KeyA; + case Keycode.B: return KeyboardKey.KeyB; + case Keycode.C: return KeyboardKey.KeyC; + case Keycode.D: return KeyboardKey.KeyD; + case Keycode.E: return KeyboardKey.KeyE; + case Keycode.F: return KeyboardKey.KeyF; + case Keycode.G: return KeyboardKey.KeyG; + case Keycode.H: return KeyboardKey.KeyH; + case Keycode.I: return KeyboardKey.KeyI; + case Keycode.J: return KeyboardKey.KeyJ; + case Keycode.K: return KeyboardKey.KeyK; + case Keycode.L: return KeyboardKey.KeyL; + case Keycode.M: return KeyboardKey.KeyM; + case Keycode.N: return KeyboardKey.KeyN; + case Keycode.O: return KeyboardKey.KeyO; + case Keycode.P: return KeyboardKey.KeyP; + case Keycode.Q: return KeyboardKey.KeyQ; + case Keycode.R: return KeyboardKey.KeyR; + case Keycode.S: return KeyboardKey.KeyS; + case Keycode.T: return KeyboardKey.KeyT; + case Keycode.U: return KeyboardKey.KeyU; + case Keycode.V: return KeyboardKey.KeyV; + case Keycode.W: return KeyboardKey.KeyW; + case Keycode.X: return KeyboardKey.KeyX; + case Keycode.Y: return KeyboardKey.KeyY; + case Keycode.Z: return KeyboardKey.KeyZ; + + case Keycode.CapsLock: return KeyboardKey.CapsLock; + case Keycode.Insert: return KeyboardKey.Insert; + case Keycode.Del: return KeyboardKey.Delete; + // Android doesn�t have a dedicated Print Screen key in most cases. + case Keycode.Home: return KeyboardKey.Home; + case Keycode.MoveEnd: return KeyboardKey.End; + case Keycode.PageDown: return KeyboardKey.PageDown; + case Keycode.PageUp: return KeyboardKey.PageUp; + case Keycode.Escape: return KeyboardKey.Escape; + case Keycode.MediaPause: return KeyboardKey.Pause; + + case Keycode.Menu: return KeyboardKey.AltLeft; // Often maps to the Alt key + case Keycode.ShiftLeft: return KeyboardKey.ShiftLeft; + case Keycode.ShiftRight: return KeyboardKey.ShiftRight; + case Keycode.CtrlLeft: return KeyboardKey.ControlLeft; + case Keycode.CtrlRight: return KeyboardKey.ControlRight; + case Keycode.Enter: return KeyboardKey.Enter; + case Keycode.Tab: return KeyboardKey.Tab; + case Keycode.Back: return KeyboardKey.Backspace; + + case Keycode.F1: return KeyboardKey.F1; + case Keycode.F2: return KeyboardKey.F2; + case Keycode.F3: return KeyboardKey.F3; + case Keycode.F4: return KeyboardKey.F4; + case Keycode.F5: return KeyboardKey.F5; + case Keycode.F6: return KeyboardKey.F6; + case Keycode.F7: return KeyboardKey.F7; + case Keycode.F8: return KeyboardKey.F8; + case Keycode.F9: return KeyboardKey.F9; + case Keycode.F10: return KeyboardKey.F10; + case Keycode.F11: return KeyboardKey.F11; + case Keycode.F12: return KeyboardKey.F12; + + case Keycode.NumLock: return KeyboardKey.NumLock; + case Keycode.ScrollLock: return KeyboardKey.ScrollLock; + + case Keycode.MetaLeft: return KeyboardKey.MetaLeft; + case Keycode.MetaRight: return KeyboardKey.MetaRight; + case Keycode.NumpadDivide: return KeyboardKey.NumpadDivide; + case Keycode.NumpadMultiply: return KeyboardKey.NumpadMultiply; + case Keycode.NumpadSubtract: return KeyboardKey.NumpadSubtract; + case Keycode.NumpadAdd: return KeyboardKey.NumpadAdd; + + // Punctuation and symbol keys + case Keycode.Equals: return KeyboardKey.Equal; + case Keycode.Minus: return KeyboardKey.Minus; + case Keycode.Grave: return KeyboardKey.Backquote; + case Keycode.Comma: return KeyboardKey.Comma; + case Keycode.Period: return KeyboardKey.Period; + case Keycode.Slash: return KeyboardKey.Slash; + case Keycode.LeftBracket: return KeyboardKey.BracketLeft; + case Keycode.RightBracket: return KeyboardKey.BracketRight; + case Keycode.Backslash: return KeyboardKey.Backslash; + case Keycode.Semicolon: return KeyboardKey.Semicolon; + + default: + return KeyboardKey.Unknown; + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/Android/KeyboardOptions.cs b/engine/Orbit.Input/Platforms/Android/KeyboardOptions.cs new file mode 100644 index 0000000..4d31673 --- /dev/null +++ b/engine/Orbit.Input/Platforms/Android/KeyboardOptions.cs @@ -0,0 +1,6 @@ +namespace Orbit.Input; + +public partial class KeyboardOptions +{ + public bool AutoAttachToLifecycleEvents { get; set; } = true; +} diff --git a/engine/Orbit.Input/Platforms/MacCatalyst/GameController.cs b/engine/Orbit.Input/Platforms/MacCatalyst/GameController.cs new file mode 100644 index 0000000..a7f4077 --- /dev/null +++ b/engine/Orbit.Input/Platforms/MacCatalyst/GameController.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using Foundation; +using GameController; + +namespace Orbit.Input; + +public partial class GameController +{ + private readonly GCController controller; + + public GameController(GCController controller) + { + this.controller = controller; + + Dpad = new Stick(this, nameof(Dpad)); + LeftStick = new Stick(this, nameof(LeftStick)); + RightStick = new Stick(this, nameof(RightStick)); + + LeftShoulder = new Shoulder(this, nameof(LeftShoulder)); + RightShoulder = new Shoulder(this, nameof(RightShoulder)); + + North = new ButtonValue(this, nameof(North)); + South = new ButtonValue(this, nameof(South)); + East = new ButtonValue(this, nameof(East)); + West = new ButtonValue(this, nameof(West)); + Pause = new ButtonValue(this, nameof(Pause)); + + if (OperatingSystem.IsMacOSVersionAtLeast(16)) + { + controller.PhysicalInputProfile.ValueDidChangeHandler += Changed; + } + } + + private void Changed(GCPhysicalInputProfile gamepad, GCControllerElement element) + { + Debug.WriteLine($"{element}"); + + switch (element) + { + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button A")): + South.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button B")): + East.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button X")): + West.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button Y")): + North.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Right Shoulder")): + RightShoulder.Button.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Right Trigger")): + RightShoulder.Trigger.Value = buttonInput.Value; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Left Shoulder")): + LeftShoulder.Button.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Left Trigger")): + LeftShoulder.Trigger.Value = buttonInput.Value; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button Menu")): + Pause.Value = buttonInput.IsPressed; + break; + + case GCControllerDirectionPad directionPad when directionPad.Aliases.Contains(new NSString("Left Thumbstick")): + LeftStick.XAxis.Value = directionPad.XAxis.Value; + LeftStick.YAxis.Value = directionPad.YAxis.Value; + break; + + case GCControllerDirectionPad directionPad when directionPad.Aliases.Contains(new NSString("Right Thumbstick")): + RightStick.XAxis.Value = directionPad.XAxis.Value; + RightStick.YAxis.Value = directionPad.YAxis.Value; + break; + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/MacCatalyst/GameControllerManager.cs b/engine/Orbit.Input/Platforms/MacCatalyst/GameControllerManager.cs new file mode 100644 index 0000000..83ea4cd --- /dev/null +++ b/engine/Orbit.Input/Platforms/MacCatalyst/GameControllerManager.cs @@ -0,0 +1,27 @@ +using Foundation; + +using GameController; + +namespace Orbit.Input; + +public partial class GameControllerManager +{ + private GameControllerManager() + { + GCController.Notifications.ObserveDidConnect(ConnectToController); + } + + public partial async Task StartDiscovery() + { + await GCController.StartWirelessControllerDiscoveryAsync(); + } + + private void ConnectToController(object? sender, NSNotificationEventArgs e) + { + if (e.Notification.Object is GCController controller) + { + var gameController = new GameController(controller); + OnGameControllerConnected(gameController); + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/MacCatalyst/KeyboardManager.cs b/engine/Orbit.Input/Platforms/MacCatalyst/KeyboardManager.cs new file mode 100644 index 0000000..97eb7eb --- /dev/null +++ b/engine/Orbit.Input/Platforms/MacCatalyst/KeyboardManager.cs @@ -0,0 +1,151 @@ +using Foundation; + +using UIKit; + +namespace Orbit.Input; + +public partial class KeyboardManager +{ + public void PressesBegan(NSSet presses, UIPressesEvent evt) + { + foreach (UIPress press in presses) + { + if (press.Key is null) continue; + + var mapped = ToKeyboardKey(press.Key.KeyCode); + + KeyboardKeyPressed(mapped); + } + } + + public void PressesCancelled(NSSet presses, UIPressesEvent evt) + { + ReleaseKeys(presses); + } + + void ReleaseKeys(NSSet presses) + { + foreach (UIPress press in presses) + { + if (press.Key is null) continue; + + var mapped = ToKeyboardKey(press.Key.KeyCode); + + KeyboardKeyReleased(mapped); + } + } + + public void PressesEnded(NSSet presses, UIPressesEvent evt) + { + ReleaseKeys(presses); + } + + static KeyboardKey ToKeyboardKey(UIKeyboardHidUsage platformKey) => platformKey switch + { + UIKeyboardHidUsage.KeyboardA => KeyboardKey.KeyA, + UIKeyboardHidUsage.KeyboardB => KeyboardKey.KeyB, + UIKeyboardHidUsage.KeyboardC => KeyboardKey.KeyC, + UIKeyboardHidUsage.KeyboardD => KeyboardKey.KeyD, + UIKeyboardHidUsage.KeyboardE => KeyboardKey.KeyE, + UIKeyboardHidUsage.KeyboardF => KeyboardKey.KeyF, + UIKeyboardHidUsage.KeyboardG => KeyboardKey.KeyG, + UIKeyboardHidUsage.KeyboardH => KeyboardKey.KeyH, + UIKeyboardHidUsage.KeyboardJ => KeyboardKey.KeyJ, + UIKeyboardHidUsage.KeyboardK => KeyboardKey.KeyK, + UIKeyboardHidUsage.KeyboardL => KeyboardKey.KeyL, + UIKeyboardHidUsage.KeyboardM => KeyboardKey.KeyM, + UIKeyboardHidUsage.KeyboardI => KeyboardKey.KeyI, + UIKeyboardHidUsage.KeyboardN => KeyboardKey.KeyN, + UIKeyboardHidUsage.KeyboardO => KeyboardKey.KeyO, + UIKeyboardHidUsage.KeyboardP => KeyboardKey.KeyP, + UIKeyboardHidUsage.KeyboardQ => KeyboardKey.KeyQ, + UIKeyboardHidUsage.KeyboardR => KeyboardKey.KeyR, + UIKeyboardHidUsage.KeyboardS => KeyboardKey.KeyS, + UIKeyboardHidUsage.KeyboardT => KeyboardKey.KeyT, + UIKeyboardHidUsage.KeyboardU => KeyboardKey.KeyU, + UIKeyboardHidUsage.KeyboardV => KeyboardKey.KeyV, + UIKeyboardHidUsage.KeyboardW => KeyboardKey.KeyW, + UIKeyboardHidUsage.KeyboardX => KeyboardKey.KeyX, + UIKeyboardHidUsage.KeyboardY => KeyboardKey.KeyY, + UIKeyboardHidUsage.KeyboardZ => KeyboardKey.KeyZ, + UIKeyboardHidUsage.Keyboard1 => KeyboardKey.Digit1, + UIKeyboardHidUsage.Keyboard2 => KeyboardKey.Digit2, + UIKeyboardHidUsage.Keyboard3 => KeyboardKey.Digit3, + UIKeyboardHidUsage.Keyboard4 => KeyboardKey.Digit4, + UIKeyboardHidUsage.Keyboard5 => KeyboardKey.Digit5, + UIKeyboardHidUsage.Keyboard6 => KeyboardKey.Digit6, + UIKeyboardHidUsage.Keyboard7 => KeyboardKey.Digit7, + UIKeyboardHidUsage.Keyboard8 => KeyboardKey.Digit8, + UIKeyboardHidUsage.Keyboard9 => KeyboardKey.Digit9, + UIKeyboardHidUsage.Keyboard0 => KeyboardKey.Digit0, + UIKeyboardHidUsage.KeyboardReturnOrEnter => KeyboardKey.Enter, + UIKeyboardHidUsage.KeyboardEscape => KeyboardKey.Escape, + UIKeyboardHidUsage.KeyboardDeleteOrBackspace => KeyboardKey.Backspace, + UIKeyboardHidUsage.KeyboardTab => KeyboardKey.Tab, + UIKeyboardHidUsage.KeyboardSpacebar => KeyboardKey.Space, + UIKeyboardHidUsage.KeyboardHyphen => KeyboardKey.Minus, + UIKeyboardHidUsage.KeyboardEqualSign => KeyboardKey.Equal, + UIKeyboardHidUsage.KeyboardOpenBracket => KeyboardKey.BracketLeft, + UIKeyboardHidUsage.KeyboardCloseBracket => KeyboardKey.BracketRight, + UIKeyboardHidUsage.KeyboardBackslash => KeyboardKey.Backslash, + UIKeyboardHidUsage.KeyboardSemicolon => KeyboardKey.Semicolon, + UIKeyboardHidUsage.KeyboardQuote => KeyboardKey.Quote, + UIKeyboardHidUsage.KeyboardComma => KeyboardKey.Comma, + UIKeyboardHidUsage.KeyboardPeriod => KeyboardKey.Period, + UIKeyboardHidUsage.KeyboardSlash => KeyboardKey.Slash, + UIKeyboardHidUsage.KeyboardCapsLock => KeyboardKey.CapsLock, + UIKeyboardHidUsage.KeyboardF1 => KeyboardKey.F1, + UIKeyboardHidUsage.KeyboardF2 => KeyboardKey.F2, + UIKeyboardHidUsage.KeyboardF3 => KeyboardKey.F3, + UIKeyboardHidUsage.KeyboardF4 => KeyboardKey.F4, + UIKeyboardHidUsage.KeyboardF5 => KeyboardKey.F5, + UIKeyboardHidUsage.KeyboardF6 => KeyboardKey.F6, + UIKeyboardHidUsage.KeyboardF7 => KeyboardKey.F7, + UIKeyboardHidUsage.KeyboardF8 => KeyboardKey.F7, + UIKeyboardHidUsage.KeyboardF9 => KeyboardKey.F9, + UIKeyboardHidUsage.KeyboardF10 => KeyboardKey.F10, + UIKeyboardHidUsage.KeyboardF11 => KeyboardKey.F11, + UIKeyboardHidUsage.KeyboardF12 => KeyboardKey.F12, + UIKeyboardHidUsage.KeyboardPrintScreen => KeyboardKey.PrintScreen, + UIKeyboardHidUsage.KeyboardScrollLock => KeyboardKey.ScrollLock, + UIKeyboardHidUsage.KeyboardPause => KeyboardKey.Pause, + UIKeyboardHidUsage.KeyboardInsert => KeyboardKey.Insert, + UIKeyboardHidUsage.KeyboardHome => KeyboardKey.Home, + UIKeyboardHidUsage.KeyboardPageUp => KeyboardKey.PageUp, + UIKeyboardHidUsage.KeyboardDeleteForward => KeyboardKey.Delete, + UIKeyboardHidUsage.KeyboardEnd => KeyboardKey.End, + UIKeyboardHidUsage.KeyboardPageDown => KeyboardKey.PageDown, + UIKeyboardHidUsage.KeyboardRightArrow => KeyboardKey.ArrowRight, + UIKeyboardHidUsage.KeyboardLeftArrow => KeyboardKey.ArrowLeft, + UIKeyboardHidUsage.KeyboardDownArrow => KeyboardKey.ArrowDown, + UIKeyboardHidUsage.KeyboardUpArrow => KeyboardKey.ArrowUp, + UIKeyboardHidUsage.KeypadNumLock => KeyboardKey.NumLock, + UIKeyboardHidUsage.KeypadSlash => KeyboardKey.IntBackslash, + UIKeyboardHidUsage.KeypadAsterisk => KeyboardKey.NumpadMultiply, + UIKeyboardHidUsage.KeypadHyphen => KeyboardKey.NumpadSubtract, + UIKeyboardHidUsage.KeypadPlus => KeyboardKey.NumpadAdd, + UIKeyboardHidUsage.KeypadEnter => KeyboardKey.Enter, + UIKeyboardHidUsage.Keypad1 => KeyboardKey.Numpad1, + UIKeyboardHidUsage.Keypad2 => KeyboardKey.Numpad2, + UIKeyboardHidUsage.Keypad3 => KeyboardKey.Numpad3, + UIKeyboardHidUsage.Keypad4 => KeyboardKey.Numpad4, + UIKeyboardHidUsage.Keypad5 => KeyboardKey.Numpad5, + UIKeyboardHidUsage.Keypad6 => KeyboardKey.Numpad6, + UIKeyboardHidUsage.Keypad7 => KeyboardKey.Numpad7, + UIKeyboardHidUsage.Keypad8 => KeyboardKey.Numpad8, + UIKeyboardHidUsage.Keypad9 => KeyboardKey.Numpad9, + UIKeyboardHidUsage.Keypad0 => KeyboardKey.Numpad0, + UIKeyboardHidUsage.KeypadPeriod => KeyboardKey.NumpadDecimal, + UIKeyboardHidUsage.KeyboardNonUSBackslash => KeyboardKey.IntBackslash, + UIKeyboardHidUsage.KeyboardApplication => KeyboardKey.LaunchApplication1, + UIKeyboardHidUsage.KeypadEqualSign => KeyboardKey.Equal, + UIKeyboardHidUsage.KeyboardMenu => KeyboardKey.ContextMenu, + UIKeyboardHidUsage.KeyboardLeftControl => KeyboardKey.ControlLeft, + UIKeyboardHidUsage.KeyboardLeftShift => KeyboardKey.ShiftLeft, + UIKeyboardHidUsage.KeyboardLeftAlt => KeyboardKey.AltLeft, + UIKeyboardHidUsage.KeyboardRightControl => KeyboardKey.ControlRight, + UIKeyboardHidUsage.KeyboardRightShift => KeyboardKey.ShiftRight, + UIKeyboardHidUsage.KeyboardRightAlt => KeyboardKey.AltRight, + _ => KeyboardKey.Unknown + }; +} diff --git a/engine/Orbit.Input/Platforms/MacCatalyst/KeyboardPageViewController.cs b/engine/Orbit.Input/Platforms/MacCatalyst/KeyboardPageViewController.cs new file mode 100644 index 0000000..26a10dd --- /dev/null +++ b/engine/Orbit.Input/Platforms/MacCatalyst/KeyboardPageViewController.cs @@ -0,0 +1,30 @@ +using Foundation; +using Microsoft.Maui.Platform; +using UIKit; + +namespace Orbit.Input +{ + public class KeyboardPageViewController : PageViewController + { + internal KeyboardPageViewController(IView page, IMauiContext mauiContext) + : base(page, mauiContext) { } + + public override void PressesBegan(NSSet presses, UIPressesEvent evt) + { + KeyboardManager.Current.PressesBegan(presses, evt); + base.PressesBegan(presses, evt); + } + + public override void PressesCancelled(NSSet presses, UIPressesEvent evt) + { + KeyboardManager.Current.PressesCancelled(presses, evt); + base.PressesCancelled(presses, evt); + } + + public override void PressesEnded(NSSet presses, UIPressesEvent evt) + { + KeyboardManager.Current.PressesEnded(presses, evt); + base.PressesEnded(presses, evt); + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/Windows/GameController.cs b/engine/Orbit.Input/Platforms/Windows/GameController.cs new file mode 100644 index 0000000..78e6403 --- /dev/null +++ b/engine/Orbit.Input/Platforms/Windows/GameController.cs @@ -0,0 +1,80 @@ +using Microsoft.UI.Xaml.Controls.Primitives; + +using Windows.Gaming.Input; + +namespace Orbit.Input; + +public partial class GameController +{ + private readonly Gamepad gamepad; + private CancellationTokenSource? cancellationTokenSource; + + public GameController(Gamepad gamepad) + { + this.gamepad = gamepad; + + Dpad = new Stick(this, nameof(Dpad)); + LeftStick = new Stick(this, nameof(LeftStick)); + RightStick = new Stick(this, nameof(RightStick)); + + LeftShoulder = new Shoulder(this, nameof(LeftShoulder)); + RightShoulder = new Shoulder(this, nameof(RightShoulder)); + + North = new ButtonValue(this, nameof(North)); + South = new ButtonValue(this, nameof(South)); + East = new ButtonValue(this, nameof(East)); + West = new ButtonValue(this, nameof(West)); + Pause = new ButtonValue(this, nameof(Pause)); + } + + public void StartUpdates(TimeSpan updateFrequency) + { + if (this.cancellationTokenSource is not null) + { + return; + } + + this.cancellationTokenSource = new(); + + Task.Run(async () => + { + while (this.cancellationTokenSource?.IsCancellationRequested is false) + { + Update(); + + await Task.Delay(updateFrequency); + } + }); + } + + public void StopUpdates() + { + this.cancellationTokenSource?.Cancel(); + this.cancellationTokenSource = null; + } + + private void Update() + { + var reading = gamepad.GetCurrentReading(); + + LeftStick.XAxis.Value = (float)reading.LeftThumbstickX; + LeftStick.YAxis.Value = (float)reading.LeftThumbstickY; + + RightStick.XAxis.Value = (float)reading.RightThumbstickX; + RightStick.YAxis.Value = (float)reading.RightThumbstickY; + + LeftShoulder.Button.Value = reading.Buttons.HasFlag(GamepadButtons.LeftShoulder); + LeftShoulder.Trigger.Value = (float)reading.LeftTrigger; + + RightShoulder.Button.Value = reading.Buttons.HasFlag(GamepadButtons.RightShoulder); + RightShoulder.Trigger.Value = (float)reading.RightTrigger; + + North.Value = reading.Buttons.HasFlag(GamepadButtons.Y); + East.Value = reading.Buttons.HasFlag(GamepadButtons.B); + South.Value = reading.Buttons.HasFlag(GamepadButtons.A); + West.Value = reading.Buttons.HasFlag(GamepadButtons.X); + + Dpad.XAxis.Value = reading.Buttons.HasFlag(GamepadButtons.DPadRight) ? 1f : reading.Buttons.HasFlag(GamepadButtons.DPadLeft) ? -1f : 0f; + Dpad.YAxis.Value = reading.Buttons.HasFlag(GamepadButtons.DPadDown) ? 1f : reading.Buttons.HasFlag(GamepadButtons.DPadUp) ? -1f : 0f; + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/Windows/GameControllerManager.cs b/engine/Orbit.Input/Platforms/Windows/GameControllerManager.cs new file mode 100644 index 0000000..eccc53b --- /dev/null +++ b/engine/Orbit.Input/Platforms/Windows/GameControllerManager.cs @@ -0,0 +1,32 @@ +using Windows.Gaming.Input; + +namespace Orbit.Input; + +public partial class GameControllerManager +{ + private GameControllerManager() + { + } + + public bool StartControllerMonitoringUponDetection { get; set; } = true; + + public TimeSpan ControllerUpdateFrequency { get; set; } = TimeSpan.FromMicroseconds(100); + + public partial Task StartDiscovery() + { + Gamepad.GamepadAdded += OnGamepadAdded; + + return Task.CompletedTask; + } + + private void OnGamepadAdded(object? sender, Gamepad gamepad) + { + var controller = new GameController(gamepad); + OnGameControllerConnected(controller); + + if (StartControllerMonitoringUponDetection) + { + controller.StartUpdates(ControllerUpdateFrequency); + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/Windows/GameControllerOptions.cs b/engine/Orbit.Input/Platforms/Windows/GameControllerOptions.cs new file mode 100644 index 0000000..0e18595 --- /dev/null +++ b/engine/Orbit.Input/Platforms/Windows/GameControllerOptions.cs @@ -0,0 +1,8 @@ +namespace Orbit.Input; + +public partial class GameControllerOptions +{ + public bool StartControllerMonitoringUponDetection { get; set; } = true; + + public TimeSpan ControllerUpdateFrequency { get; set; } = TimeSpan.FromMicroseconds(16); +} diff --git a/engine/Orbit.Input/Platforms/Windows/KeyboardManager.cs b/engine/Orbit.Input/Platforms/Windows/KeyboardManager.cs new file mode 100644 index 0000000..05d2766 --- /dev/null +++ b/engine/Orbit.Input/Platforms/Windows/KeyboardManager.cs @@ -0,0 +1,144 @@ +using Windows.System; +using Microsoft.UI.Xaml; + +namespace Orbit.Input; + +public partial class KeyboardManager +{ + public void AttachKeyboard(UIElement window) + { + window.PreviewKeyUp += OnWindowPreviewKeyUp; + window.PreviewKeyDown += OnWindowPreviewKeyDown; + } + + public void DetachKeyboard(UIElement window) + { + window.PreviewKeyUp -= OnWindowPreviewKeyUp; + window.PreviewKeyDown -= OnWindowPreviewKeyDown; + } + + private void OnWindowPreviewKeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e) => KeyboardKeyPressed(ToKeyboardKey(e.Key)); + + private void OnWindowPreviewKeyUp(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e) => KeyboardKeyReleased(ToKeyboardKey(e.Key)); + + /// + /// Same as map to Java tbh + /// + /// + /// + public static KeyboardKey ToKeyboardKey(VirtualKey virtualKey) + { + switch (virtualKey) + { + case VirtualKey.Space: return KeyboardKey.Space; + case VirtualKey.Left: return KeyboardKey.ArrowLeft; + case VirtualKey.Up: return KeyboardKey.ArrowUp; + case VirtualKey.Right: return KeyboardKey.ArrowRight; + case VirtualKey.Down: return KeyboardKey.ArrowDown; + case VirtualKey.Number0: return KeyboardKey.Digit0; + case VirtualKey.Number1: return KeyboardKey.Digit1; + case VirtualKey.Number2: return KeyboardKey.Digit2; + case VirtualKey.Number3: return KeyboardKey.Digit3; + case VirtualKey.Number4: return KeyboardKey.Digit4; + case VirtualKey.Number5: return KeyboardKey.Digit5; + case VirtualKey.Number6: return KeyboardKey.Digit6; + case VirtualKey.Number7: return KeyboardKey.Digit7; + case VirtualKey.Number8: return KeyboardKey.Digit8; + case VirtualKey.Number9: return KeyboardKey.Digit9; + case VirtualKey.A: return KeyboardKey.KeyA; + case VirtualKey.B: return KeyboardKey.KeyB; + case VirtualKey.C: return KeyboardKey.KeyC; + case VirtualKey.D: return KeyboardKey.KeyD; + case VirtualKey.E: return KeyboardKey.KeyE; + case VirtualKey.F: return KeyboardKey.KeyF; + case VirtualKey.G: return KeyboardKey.KeyG; + case VirtualKey.H: return KeyboardKey.KeyH; + case VirtualKey.I: return KeyboardKey.KeyI; + case VirtualKey.J: return KeyboardKey.KeyJ; + case VirtualKey.K: return KeyboardKey.KeyK; + case VirtualKey.L: return KeyboardKey.KeyL; + case VirtualKey.M: return KeyboardKey.KeyM; + case VirtualKey.N: return KeyboardKey.KeyN; + case VirtualKey.O: return KeyboardKey.KeyO; + case VirtualKey.P: return KeyboardKey.KeyP; + case VirtualKey.Q: return KeyboardKey.KeyQ; + case VirtualKey.R: return KeyboardKey.KeyR; + case VirtualKey.S: return KeyboardKey.KeyS; + case VirtualKey.T: return KeyboardKey.KeyT; + case VirtualKey.U: return KeyboardKey.KeyU; + case VirtualKey.V: return KeyboardKey.KeyV; + case VirtualKey.W: return KeyboardKey.KeyW; + case VirtualKey.X: return KeyboardKey.KeyX; + case VirtualKey.Y: return KeyboardKey.KeyY; + case VirtualKey.Z: return KeyboardKey.KeyZ; + case VirtualKey.CapitalLock: return KeyboardKey.CapsLock; + case VirtualKey.Insert: return KeyboardKey.Insert; + case VirtualKey.Delete: return KeyboardKey.Delete; + case VirtualKey.Snapshot: return KeyboardKey.PrintScreen; + case VirtualKey.Home: return KeyboardKey.Home; + case VirtualKey.End: return KeyboardKey.End; + case VirtualKey.PageDown: return KeyboardKey.PageDown; + case VirtualKey.PageUp: return KeyboardKey.PageUp; + case VirtualKey.Escape: return KeyboardKey.Escape; + case VirtualKey.Pause: return KeyboardKey.Pause; + case VirtualKey.Menu: return KeyboardKey.AltLeft; + case VirtualKey.LeftMenu: return KeyboardKey.AltLeft; + case VirtualKey.RightMenu: return KeyboardKey.AltRight; + case VirtualKey.Shift: return KeyboardKey.ShiftLeft; + case VirtualKey.LeftShift: return KeyboardKey.ShiftLeft; + case VirtualKey.RightShift: return KeyboardKey.ShiftRight; + case VirtualKey.LeftControl: return KeyboardKey.ControlLeft; + case VirtualKey.RightControl: return KeyboardKey.ControlRight; + case VirtualKey.Control: return KeyboardKey.ControlLeft; + case VirtualKey.Enter: return KeyboardKey.Enter; + case VirtualKey.Tab: return KeyboardKey.Tab; + case VirtualKey.Back: return KeyboardKey.Backspace; + case VirtualKey.F1: return KeyboardKey.F1; + case VirtualKey.F2: return KeyboardKey.F2; + case VirtualKey.F3: return KeyboardKey.F3; + case VirtualKey.F4: return KeyboardKey.F4; + case VirtualKey.F5: return KeyboardKey.F5; + case VirtualKey.F6: return KeyboardKey.F6; + case VirtualKey.F7: return KeyboardKey.F7; + case VirtualKey.F8: return KeyboardKey.F8; + case VirtualKey.F9: return KeyboardKey.F9; + case VirtualKey.F10: return KeyboardKey.F10; + case VirtualKey.F11: return KeyboardKey.F11; + case VirtualKey.F12: return KeyboardKey.F12; + case VirtualKey.NumberKeyLock: return KeyboardKey.NumLock; + case VirtualKey.Scroll: return KeyboardKey.ScrollLock; + case VirtualKey.NumberPad0: return KeyboardKey.Numpad0; + case VirtualKey.NumberPad1: return KeyboardKey.Numpad1; + case VirtualKey.NumberPad2: return KeyboardKey.Numpad2; + case VirtualKey.NumberPad3: return KeyboardKey.Numpad3; + case VirtualKey.NumberPad4: return KeyboardKey.Numpad4; + case VirtualKey.NumberPad5: return KeyboardKey.Numpad5; + case VirtualKey.NumberPad6: return KeyboardKey.Numpad6; + case VirtualKey.NumberPad7: return KeyboardKey.Numpad7; + case VirtualKey.NumberPad8: return KeyboardKey.Numpad8; + case VirtualKey.NumberPad9: return KeyboardKey.Numpad9; + case VirtualKey.LeftWindows: return KeyboardKey.MetaLeft; + case VirtualKey.RightWindows: return KeyboardKey.MetaRight; + case VirtualKey.Divide: return KeyboardKey.NumpadDivide; + case VirtualKey.Multiply: return KeyboardKey.NumpadMultiply; + case VirtualKey.Subtract: return KeyboardKey.NumpadSubtract; + case VirtualKey.Add: return KeyboardKey.NumpadAdd; + } + + switch ((int)virtualKey) + { + case 187: return KeyboardKey.Equal; + case 189: return KeyboardKey.Minus; + case 192: return KeyboardKey.Backquote; + case 188: return KeyboardKey.Comma; + case 190: return KeyboardKey.Period; + case 191: return KeyboardKey.Slash; + case 219: return KeyboardKey.BracketLeft; + case 221: return KeyboardKey.BracketRight; + case 220: return KeyboardKey.Backslash; + case 186: return KeyboardKey.Semicolon; + } + + return KeyboardKey.Unknown; + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/iOS/GameController.cs b/engine/Orbit.Input/Platforms/iOS/GameController.cs new file mode 100644 index 0000000..3e4c379 --- /dev/null +++ b/engine/Orbit.Input/Platforms/iOS/GameController.cs @@ -0,0 +1,91 @@ +using System.Diagnostics; +using Foundation; +using GameController; + +namespace Orbit.Input; + +public partial class GameController +{ + private readonly GCController controller; + + /// + /// Creates a new instance of . + /// + /// The that provides platform specific interaction. + public GameController(GCController controller) + { + this.controller = controller; + + Dpad = new Stick(this, nameof(Dpad)); + LeftStick = new Stick(this, nameof(LeftStick)); + RightStick = new Stick(this, nameof(RightStick)); + + LeftShoulder = new Shoulder(this, nameof(LeftShoulder)); + RightShoulder = new Shoulder(this, nameof(RightShoulder)); + + North = new ButtonValue(this, nameof(North)); + South = new ButtonValue(this, nameof(South)); + East = new ButtonValue(this, nameof(East)); + West = new ButtonValue(this, nameof(West)); + Pause = new ButtonValue(this, nameof(Pause)); + + if (OperatingSystem.IsIOSVersionAtLeast(16)) + { + controller.PhysicalInputProfile.ValueDidChangeHandler += Changed; + } + } + + private void Changed(GCPhysicalInputProfile gamepad, GCControllerElement element) + { + Debug.WriteLine($"{element}"); + + switch (element) + { + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button A")): + South.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button B")): + East.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button X")): + West.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button Y")): + North.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Right Shoulder")): + RightShoulder.Button.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Right Trigger")): + RightShoulder.Trigger.Value = buttonInput.Value; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Left Shoulder")): + LeftShoulder.Button.Value = buttonInput.IsPressed; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Left Trigger")): + LeftShoulder.Trigger.Value = buttonInput.Value; + break; + + case GCControllerButtonInput buttonInput when buttonInput.Aliases.Contains(new NSString("Button Menu")): + Pause.Value = buttonInput.IsPressed; + break; + + case GCControllerDirectionPad directionPad when directionPad.Aliases.Contains(new NSString("Left Thumbstick")): + LeftStick.XAxis.Value = directionPad.XAxis.Value; + LeftStick.YAxis.Value = directionPad.YAxis.Value; + break; + + case GCControllerDirectionPad directionPad when directionPad.Aliases.Contains(new NSString("Right Thumbstick")): + RightStick.XAxis.Value = directionPad.XAxis.Value; + RightStick.YAxis.Value = directionPad.YAxis.Value; + break; + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/iOS/GameControllerManager.cs b/engine/Orbit.Input/Platforms/iOS/GameControllerManager.cs new file mode 100644 index 0000000..83ea4cd --- /dev/null +++ b/engine/Orbit.Input/Platforms/iOS/GameControllerManager.cs @@ -0,0 +1,27 @@ +using Foundation; + +using GameController; + +namespace Orbit.Input; + +public partial class GameControllerManager +{ + private GameControllerManager() + { + GCController.Notifications.ObserveDidConnect(ConnectToController); + } + + public partial async Task StartDiscovery() + { + await GCController.StartWirelessControllerDiscoveryAsync(); + } + + private void ConnectToController(object? sender, NSNotificationEventArgs e) + { + if (e.Notification.Object is GCController controller) + { + var gameController = new GameController(controller); + OnGameControllerConnected(gameController); + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/Platforms/iOS/KeyboardManager.cs b/engine/Orbit.Input/Platforms/iOS/KeyboardManager.cs new file mode 100644 index 0000000..c152f38 --- /dev/null +++ b/engine/Orbit.Input/Platforms/iOS/KeyboardManager.cs @@ -0,0 +1,152 @@ +using Foundation; + +using UIKit; + +namespace Orbit.Input; + +public partial class KeyboardManager +{ + + public void PressesBegan(NSSet presses, UIPressesEvent evt) + { + foreach (UIPress press in presses) + { + if (press.Key is null) continue; + + var mapped = ToKeyboardKey(press.Key.KeyCode); + + KeyboardKeyPressed(mapped); + } + } + + public void PressesCancelled(NSSet presses, UIPressesEvent evt) + { + ReleaseKeys(presses); + } + + void ReleaseKeys(NSSet presses) + { + foreach (UIPress press in presses) + { + if (press.Key is null) continue; + + var mapped = ToKeyboardKey(press.Key.KeyCode); + + KeyboardKeyReleased(mapped); + } + } + + public void PressesEnded(NSSet presses, UIPressesEvent evt) + { + ReleaseKeys(presses); + } + + static KeyboardKey ToKeyboardKey(UIKeyboardHidUsage platformKey) => platformKey switch + { + UIKeyboardHidUsage.KeyboardA => KeyboardKey.KeyA, + UIKeyboardHidUsage.KeyboardB => KeyboardKey.KeyB, + UIKeyboardHidUsage.KeyboardC => KeyboardKey.KeyC, + UIKeyboardHidUsage.KeyboardD => KeyboardKey.KeyD, + UIKeyboardHidUsage.KeyboardE => KeyboardKey.KeyE, + UIKeyboardHidUsage.KeyboardF => KeyboardKey.KeyF, + UIKeyboardHidUsage.KeyboardG => KeyboardKey.KeyG, + UIKeyboardHidUsage.KeyboardH => KeyboardKey.KeyH, + UIKeyboardHidUsage.KeyboardJ => KeyboardKey.KeyJ, + UIKeyboardHidUsage.KeyboardK => KeyboardKey.KeyK, + UIKeyboardHidUsage.KeyboardL => KeyboardKey.KeyL, + UIKeyboardHidUsage.KeyboardM => KeyboardKey.KeyM, + UIKeyboardHidUsage.KeyboardI => KeyboardKey.KeyI, + UIKeyboardHidUsage.KeyboardN => KeyboardKey.KeyN, + UIKeyboardHidUsage.KeyboardO => KeyboardKey.KeyO, + UIKeyboardHidUsage.KeyboardP => KeyboardKey.KeyP, + UIKeyboardHidUsage.KeyboardQ => KeyboardKey.KeyQ, + UIKeyboardHidUsage.KeyboardR => KeyboardKey.KeyR, + UIKeyboardHidUsage.KeyboardS => KeyboardKey.KeyS, + UIKeyboardHidUsage.KeyboardT => KeyboardKey.KeyT, + UIKeyboardHidUsage.KeyboardU => KeyboardKey.KeyU, + UIKeyboardHidUsage.KeyboardV => KeyboardKey.KeyV, + UIKeyboardHidUsage.KeyboardW => KeyboardKey.KeyW, + UIKeyboardHidUsage.KeyboardX => KeyboardKey.KeyX, + UIKeyboardHidUsage.KeyboardY => KeyboardKey.KeyY, + UIKeyboardHidUsage.KeyboardZ => KeyboardKey.KeyZ, + UIKeyboardHidUsage.Keyboard1 => KeyboardKey.Digit1, + UIKeyboardHidUsage.Keyboard2 => KeyboardKey.Digit2, + UIKeyboardHidUsage.Keyboard3 => KeyboardKey.Digit3, + UIKeyboardHidUsage.Keyboard4 => KeyboardKey.Digit4, + UIKeyboardHidUsage.Keyboard5 => KeyboardKey.Digit5, + UIKeyboardHidUsage.Keyboard6 => KeyboardKey.Digit6, + UIKeyboardHidUsage.Keyboard7 => KeyboardKey.Digit7, + UIKeyboardHidUsage.Keyboard8 => KeyboardKey.Digit8, + UIKeyboardHidUsage.Keyboard9 => KeyboardKey.Digit9, + UIKeyboardHidUsage.Keyboard0 => KeyboardKey.Digit0, + UIKeyboardHidUsage.KeyboardReturnOrEnter => KeyboardKey.Enter, + UIKeyboardHidUsage.KeyboardEscape => KeyboardKey.Escape, + UIKeyboardHidUsage.KeyboardDeleteOrBackspace => KeyboardKey.Backspace, + UIKeyboardHidUsage.KeyboardTab => KeyboardKey.Tab, + UIKeyboardHidUsage.KeyboardSpacebar => KeyboardKey.Space, + UIKeyboardHidUsage.KeyboardHyphen => KeyboardKey.Minus, + UIKeyboardHidUsage.KeyboardEqualSign => KeyboardKey.Equal, + UIKeyboardHidUsage.KeyboardOpenBracket => KeyboardKey.BracketLeft, + UIKeyboardHidUsage.KeyboardCloseBracket => KeyboardKey.BracketRight, + UIKeyboardHidUsage.KeyboardBackslash => KeyboardKey.Backslash, + UIKeyboardHidUsage.KeyboardSemicolon => KeyboardKey.Semicolon, + UIKeyboardHidUsage.KeyboardQuote => KeyboardKey.Quote, + UIKeyboardHidUsage.KeyboardComma => KeyboardKey.Comma, + UIKeyboardHidUsage.KeyboardPeriod => KeyboardKey.Period, + UIKeyboardHidUsage.KeyboardSlash => KeyboardKey.Slash, + UIKeyboardHidUsage.KeyboardCapsLock => KeyboardKey.CapsLock, + UIKeyboardHidUsage.KeyboardF1 => KeyboardKey.F1, + UIKeyboardHidUsage.KeyboardF2 => KeyboardKey.F2, + UIKeyboardHidUsage.KeyboardF3 => KeyboardKey.F3, + UIKeyboardHidUsage.KeyboardF4 => KeyboardKey.F4, + UIKeyboardHidUsage.KeyboardF5 => KeyboardKey.F5, + UIKeyboardHidUsage.KeyboardF6 => KeyboardKey.F6, + UIKeyboardHidUsage.KeyboardF7 => KeyboardKey.F7, + UIKeyboardHidUsage.KeyboardF8 => KeyboardKey.F7, + UIKeyboardHidUsage.KeyboardF9 => KeyboardKey.F9, + UIKeyboardHidUsage.KeyboardF10 => KeyboardKey.F10, + UIKeyboardHidUsage.KeyboardF11 => KeyboardKey.F11, + UIKeyboardHidUsage.KeyboardF12 => KeyboardKey.F12, + UIKeyboardHidUsage.KeyboardPrintScreen => KeyboardKey.PrintScreen, + UIKeyboardHidUsage.KeyboardScrollLock => KeyboardKey.ScrollLock, + UIKeyboardHidUsage.KeyboardPause => KeyboardKey.Pause, + UIKeyboardHidUsage.KeyboardInsert => KeyboardKey.Insert, + UIKeyboardHidUsage.KeyboardHome => KeyboardKey.Home, + UIKeyboardHidUsage.KeyboardPageUp => KeyboardKey.PageUp, + UIKeyboardHidUsage.KeyboardDeleteForward => KeyboardKey.Delete, + UIKeyboardHidUsage.KeyboardEnd => KeyboardKey.End, + UIKeyboardHidUsage.KeyboardPageDown => KeyboardKey.PageDown, + UIKeyboardHidUsage.KeyboardRightArrow => KeyboardKey.ArrowRight, + UIKeyboardHidUsage.KeyboardLeftArrow => KeyboardKey.ArrowLeft, + UIKeyboardHidUsage.KeyboardDownArrow => KeyboardKey.ArrowDown, + UIKeyboardHidUsage.KeyboardUpArrow => KeyboardKey.ArrowUp, + UIKeyboardHidUsage.KeypadNumLock => KeyboardKey.NumLock, + UIKeyboardHidUsage.KeypadSlash => KeyboardKey.IntBackslash, + UIKeyboardHidUsage.KeypadAsterisk => KeyboardKey.NumpadMultiply, + UIKeyboardHidUsage.KeypadHyphen => KeyboardKey.NumpadSubtract, + UIKeyboardHidUsage.KeypadPlus => KeyboardKey.NumpadAdd, + UIKeyboardHidUsage.KeypadEnter => KeyboardKey.Enter, + UIKeyboardHidUsage.Keypad1 => KeyboardKey.Numpad1, + UIKeyboardHidUsage.Keypad2 => KeyboardKey.Numpad2, + UIKeyboardHidUsage.Keypad3 => KeyboardKey.Numpad3, + UIKeyboardHidUsage.Keypad4 => KeyboardKey.Numpad4, + UIKeyboardHidUsage.Keypad5 => KeyboardKey.Numpad5, + UIKeyboardHidUsage.Keypad6 => KeyboardKey.Numpad6, + UIKeyboardHidUsage.Keypad7 => KeyboardKey.Numpad7, + UIKeyboardHidUsage.Keypad8 => KeyboardKey.Numpad8, + UIKeyboardHidUsage.Keypad9 => KeyboardKey.Numpad9, + UIKeyboardHidUsage.Keypad0 => KeyboardKey.Numpad0, + UIKeyboardHidUsage.KeypadPeriod => KeyboardKey.NumpadDecimal, + UIKeyboardHidUsage.KeyboardNonUSBackslash => KeyboardKey.IntBackslash, + UIKeyboardHidUsage.KeyboardApplication => KeyboardKey.LaunchApplication1, + UIKeyboardHidUsage.KeypadEqualSign => KeyboardKey.Equal, + UIKeyboardHidUsage.KeyboardMenu => KeyboardKey.ContextMenu, + UIKeyboardHidUsage.KeyboardLeftControl => KeyboardKey.ControlLeft, + UIKeyboardHidUsage.KeyboardLeftShift => KeyboardKey.ShiftLeft, + UIKeyboardHidUsage.KeyboardLeftAlt => KeyboardKey.AltLeft, + UIKeyboardHidUsage.KeyboardRightControl => KeyboardKey.ControlRight, + UIKeyboardHidUsage.KeyboardRightShift => KeyboardKey.ShiftRight, + UIKeyboardHidUsage.KeyboardRightAlt => KeyboardKey.AltRight, + _ => KeyboardKey.Unknown + }; +} diff --git a/engine/Orbit.Input/Platforms/iOS/KeyboardPageViewController.cs b/engine/Orbit.Input/Platforms/iOS/KeyboardPageViewController.cs new file mode 100644 index 0000000..26a10dd --- /dev/null +++ b/engine/Orbit.Input/Platforms/iOS/KeyboardPageViewController.cs @@ -0,0 +1,30 @@ +using Foundation; +using Microsoft.Maui.Platform; +using UIKit; + +namespace Orbit.Input +{ + public class KeyboardPageViewController : PageViewController + { + internal KeyboardPageViewController(IView page, IMauiContext mauiContext) + : base(page, mauiContext) { } + + public override void PressesBegan(NSSet presses, UIPressesEvent evt) + { + KeyboardManager.Current.PressesBegan(presses, evt); + base.PressesBegan(presses, evt); + } + + public override void PressesCancelled(NSSet presses, UIPressesEvent evt) + { + KeyboardManager.Current.PressesCancelled(presses, evt); + base.PressesCancelled(presses, evt); + } + + public override void PressesEnded(NSSet presses, UIPressesEvent evt) + { + KeyboardManager.Current.PressesEnded(presses, evt); + base.PressesEnded(presses, evt); + } + } +} \ No newline at end of file diff --git a/engine/Orbit.Input/readme.md b/engine/Orbit.Input/readme.md new file mode 100644 index 0000000..6e16463 --- /dev/null +++ b/engine/Orbit.Input/readme.md @@ -0,0 +1,165 @@ +# Orbit Input + +Orbit.Input provides the ability to interact with both Keyboards and Game Controllers inside .NET MAUI based applications. + +## Game Controller + +Add an image of a game controller and explain the button layout... + +### Example usage + +This section aims at explaining how to add game controller support to your project. + +### Registering with the `MauiAppBuilder` + +The first step is to register the game engine in your `MauiProgram.cs` file using the `UseOrbitGameController` extension method: + +```csharp +builder + .UseMauiApp() + .UseOrbitGameController() +``` + +```csharp +builder.Services.AddSingleton(GameControllerManager.Current); +``` + +### Discovering connected controllers + +```csharp +await GameControllerManager.Current.StartDiscovery(); +``` + +### Checking if a button is pressed + +The `GameController` class provides a set or properties making it easy to check the state of buttons or sticks. + +```csharp +if (gameController.ButtonSouth.Value) +{ +} +``` + +```csharp +if (gameController.LeftStick.XAxis.Value > 0.0000001f) +{ +} +``` + +### Responding to a button press + +The `GameController` class provides both the `ButtonChanged` and `ValueChanged` events that can be subscribed to in order to receive notifications. + +#### `ButtonChanged` + +```csharp +this.gameController.ButtonChanged += GameControllerOnButtonChanged; + +private void GameControllerOnButtonChanged(object? sender, GameControllerButtonChangedEventArgs e) +{ + if (e.ButtonName == gameController.South.Name) + { + if (e.IsPressed) + { + } + else + { + } + } +} +``` + +#### `ValueChanged` + +```csharp +this.gameController.ValueChanged += GameControllerOnValueChanged; + +private void GameControllerOnValueChanged(object? sender, GameControllerValueChangedEventArgs e) +{ + if (e.ButtonName == gameController.LeftStick.XAxis.Name) + { + if (e.Value < 0.0000001f) + { + } + else if (e.Value > 0.0000001f) + { + } + } +} +``` + +## Keyboard + + + +### Example usage + +This section aims at explaining how to add keyboard support to your project. + +### Registering with the `MauiAppBuilder` + +The first step is to register the game engine in your `MauiProgram.cs` file using the `UseOrbitKeyboard` extension method: + +```csharp +builder + .UseMauiApp() + .UseOrbitKeyboard() +``` + +The library provides the `KeyboardManager.Current` property that can be used throughout your application. If you prefer to register the implementation with your dependency injection layer you can do so as follows: + +```csharp +builder.Services.AddSingleton(KeyboardManager.Current); +``` + +### Checking if a key is pressed + +The `KeyboardManager` class provides an indexer method to check whether a specific `KeyboardKey` is pressed. + +```csharp +KeyboardManager.Current[KeyboardKey.ShiftLeft]; +``` + +### Modifier keys + +The shift, alt and control keys are considered modifiers as they modify the behavior of other keys when pressed. You can determine whether modifer keys are pressed through the `Modifiers` property. + +```csharp +KeyboardManager.Current.Modifiers.HasFlag(KeyboardModifier.ShiftLeft); +``` + +### Responding to a key press + +The `KeyboardManager` class provides both the `KeyDown` and `KeyUp` events that can be subscribed to in order to receive notifications. + +##### `KeyDown` + +```csharp +KeyboardManager.Current.KeyDown += KeyboardManagerOnKeyDown; + +private void KeyboardManagerOnKeyDown(object? sender, KeyboardKey e) +{ + if (e == KeyboardKey.KeyD) + { + } + else if (e == KeyboardKey.KeyA) + { + } +} +``` + +##### `KeyUp` + +```csharp +KeyboardManager.Current.KeyUp += KeyboardManagerOnKeyUp; + +private void KeyboardManagerOnKeyUp(object? sender, KeyboardKey e) +{ + if (e == KeyboardKey.KeyD) + { + } + else if (e == KeyboardKey.KeyA) + { + } +} +``` \ No newline at end of file diff --git a/games/Platformer.sln b/games/Platformer.sln index d768f0b..80a1613 100644 --- a/games/Platformer.sln +++ b/games/Platformer.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orbit.Engine.Tests", "..\en EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer", "Platformer\Platformer.csproj", "{3DE622E7-1CBE-4B83-B2D5-FD0AD23F1090}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orbit.Input", "..\engine\Orbit.Input\Orbit.Input.csproj", "{F63F5DA8-4C0D-469C-BB8E-D36B089F5F98}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,8 +27,13 @@ Global {622CFDD5-7E63-4EBC-A67F-8418CD1CE2DA}.Release|Any CPU.Build.0 = Release|Any CPU {3DE622E7-1CBE-4B83-B2D5-FD0AD23F1090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DE622E7-1CBE-4B83-B2D5-FD0AD23F1090}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DE622E7-1CBE-4B83-B2D5-FD0AD23F1090}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {3DE622E7-1CBE-4B83-B2D5-FD0AD23F1090}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DE622E7-1CBE-4B83-B2D5-FD0AD23F1090}.Release|Any CPU.Build.0 = Release|Any CPU + {F63F5DA8-4C0D-469C-BB8E-D36B089F5F98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F63F5DA8-4C0D-469C-BB8E-D36B089F5F98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F63F5DA8-4C0D-469C-BB8E-D36B089F5F98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F63F5DA8-4C0D-469C-BB8E-D36B089F5F98}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/games/Platformer/App.xaml.cs b/games/Platformer/App.xaml.cs index c31db64..a58d84f 100644 --- a/games/Platformer/App.xaml.cs +++ b/games/Platformer/App.xaml.cs @@ -5,7 +5,10 @@ public partial class App : Application public App() { InitializeComponent(); - - MainPage = new AppShell(); } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); + } } diff --git a/games/Platformer/CharacterState.cs b/games/Platformer/CharacterState.cs index 07e519e..a1bfc19 100644 --- a/games/Platformer/CharacterState.cs +++ b/games/Platformer/CharacterState.cs @@ -1,9 +1,11 @@ namespace Platformer; +[Flags] public enum CharacterState { - Idle, - MovingRight, - MovingLeft, - Jumping + Idle = 0, + MovingRight = 1, + MovingLeft = 2, + Jumping = 4, + Running = 8 } \ No newline at end of file diff --git a/games/Platformer/ControllerManager.cs b/games/Platformer/ControllerManager.cs deleted file mode 100644 index b8618e0..0000000 --- a/games/Platformer/ControllerManager.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Platformer; - -public partial class ControllerManager -{ - private ControllerButton currentPressedButton; - - public event Action ButtonPressed; - - public ControllerButton CurrentPressedButton - { - get => this.currentPressedButton; - private set - { - if (this.currentPressedButton != value) - { - this.currentPressedButton = value; - - if (value != ControllerButton.None) - { - ButtonPressed?.Invoke(value); - } - } - } - } - - public PointF DirectionalChange { get; set; } -} - -public enum ControllerButton -{ - None, - Start, - Right, - Left, - Up, - Down, - Accept, - NavigateForward, - NavigateBackward -} diff --git a/games/Platformer/GameObjects/FloorTile.cs b/games/Platformer/GameObjects/FloorTile.cs index 21446b0..095d85b 100644 --- a/games/Platformer/GameObjects/FloorTile.cs +++ b/games/Platformer/GameObjects/FloorTile.cs @@ -2,7 +2,7 @@ using IImage = Microsoft.Maui.Graphics.IImage; -namespace Platformer; +namespace Platformer.GameObjects; public class FloorTile : GameObject { diff --git a/games/Platformer/GameObjects/PinkMan.cs b/games/Platformer/GameObjects/PinkMan.cs index fb5f8c2..4d0c03c 100644 --- a/games/Platformer/GameObjects/PinkMan.cs +++ b/games/Platformer/GameObjects/PinkMan.cs @@ -1,10 +1,8 @@ using Orbit.Engine; -using Platformer.GameObjects; - using IImage = Microsoft.Maui.Graphics.IImage; -namespace Platformer; +namespace Platformer.GameObjects; public class PinkMan : GameObject { @@ -14,10 +12,15 @@ public class PinkMan : GameObject private CharacterState state; private float position = 0f; private float yPosition = 0f; - private readonly ControllerManager controllerManager; private readonly PlayerStateManager playerStateManager; private float upwardsMovement; private readonly IImage jump; + + private const double walkSpeed = 10000d; + private const double runSpeed = 2000d; + + private bool isJumping; + private bool hasStartedJump; private CharacterState State { @@ -55,19 +58,18 @@ private CharacterState State Remove(idleSprite); Add(runSprite); break; - - case CharacterState.Jumping: - idleSprite.Stop(); - runSprite.Stop(); + } + + if (state.HasFlag(CharacterState.Jumping)) + { + idleSprite.Stop(); + runSprite.Stop(); - Remove(idleSprite); - Remove(runSprite); + Remove(idleSprite); + Remove(runSprite); - upwardsMovement = 0.04f; - break; - - default: - throw new ArgumentOutOfRangeException(); + isJumping = true; + upwardsMovement = 0.04f; } } } @@ -75,12 +77,10 @@ private CharacterState State public PinkMan( PlayerStateManager playerStateManager, - SettingsService settingsService, - ControllerManager controllerManager) + SettingsService settingsService) { this.playerStateManager = playerStateManager; this.settingsService = settingsService; - this.controllerManager = controllerManager; state = CharacterState.Idle; idleSprite = new Sprite( @@ -136,7 +136,7 @@ public override void Render(ICanvas canvas, RectF dimensions) idleSprite.Bounds = this.Bounds; runSprite.Bounds = this.Bounds; - if (state == CharacterState.Jumping) + if (isJumping) { canvas.DrawImage(jump, this.Bounds.X, this.Bounds.Y, this.Bounds.Width, this.Bounds.Height); } @@ -162,34 +162,40 @@ public override void Update(double millisecondsSinceLastUpdate) collision.Collide(); } - var actualState = this.playerStateManager.State; - - if (controllerManager.CurrentPressedButton == ControllerButton.Left) + if (isJumping && hasStartedJump && + CurrentScene.GameObjectsSnapshot.OfType().Any(x => x.Bounds.IntersectsWith(this.Bounds))) { - actualState = CharacterState.MovingLeft; + isJumping = false; + hasStartedJump = false; } - else if (controllerManager.CurrentPressedButton == ControllerButton.Right) + + CharacterState actualState = this.playerStateManager.State; + + double divisor = walkSpeed; + + if (this.playerStateManager.State.HasFlag(CharacterState.Running)) { - actualState = CharacterState.MovingRight; + divisor = runSpeed; } State = actualState; - switch (State) + if (State.HasFlag(CharacterState.MovingRight)) { - case CharacterState.MovingRight: - position = Math.Clamp(position + (float)(millisecondsSinceLastUpdate / 10000d), 0, 1); - break; - - case CharacterState.MovingLeft: - position = Math.Clamp(position - (float)(millisecondsSinceLastUpdate / 10000d), 0, 1); - break; + position = Math.Clamp(position + (float)(millisecondsSinceLastUpdate / divisor), 0, 1); + } + else if (State.HasFlag(CharacterState.MovingLeft)) + { + position = Math.Clamp(position - (float)(millisecondsSinceLastUpdate / divisor), 0, 1); + } + + if (isJumping) + { + hasStartedJump = true; - case CharacterState.Jumping: - yPosition = Math.Clamp(yPosition + upwardsMovement, 0, 1); + yPosition = Math.Clamp(yPosition + upwardsMovement, 0, 1); - upwardsMovement -= 0.004f; - break; + upwardsMovement -= 0.004f; } } } \ No newline at end of file diff --git a/games/Platformer/MainPage.xaml.cs b/games/Platformer/MainPage.xaml.cs index 2e3fc0a..046e09b 100644 --- a/games/Platformer/MainPage.xaml.cs +++ b/games/Platformer/MainPage.xaml.cs @@ -1,12 +1,18 @@ -using Orbit.Engine; +using System.Diagnostics; + +using Orbit.Engine; +using Orbit.Input; + using Platformer.GameScenes; namespace Platformer; public partial class MainPage : ContentPage { - private readonly ControllerManager controllerManager; - private readonly IGameSceneManager gameSceneManager; + private Orbit.Input.GameController? gameController; + private readonly Orbit.Input.GameControllerManager gameControllerManager; + private readonly KeyboardManager keyboardManager; + private readonly IGameSceneManager gameSceneManager; private readonly PlayerStateManager playerStateManager; private readonly SettingsService settingsService; @@ -14,37 +20,150 @@ public MainPage( IGameSceneManager gameSceneManager, PlayerStateManager playerStateManager, SettingsService settingsService, - ControllerManager controllerManager) + Orbit.Input.GameControllerManager gameControllerManager, + KeyboardManager keyboardManager) { InitializeComponent(); this.gameSceneManager = gameSceneManager; this.playerStateManager = playerStateManager; this.settingsService = settingsService; - this.controllerManager = controllerManager; - -#if MACCATALYST - this.controllerManager.Initialise(); -#endif + this.gameControllerManager = gameControllerManager; + this.keyboardManager = keyboardManager; + + // TODO: disconnected. + this.gameControllerManager.GameControllerConnected += OnGameControllerConnected; + _ = this.gameControllerManager.StartDiscovery(); - gameSceneManager.StateChanged += OnGameSceneManagerStateChanged; gameSceneManager.LoadScene(GameView); gameSceneManager.Start(); + + this.keyboardManager.KeyDown += KeyboardManagerOnKeyDown; + this.keyboardManager.KeyUp += KeyboardManagerOnKeyUp; } - private void OnGameSceneManagerStateChanged(object? sender, GameStateChangedEventArgs e) - { + private void KeyboardManagerOnKeyDown(object? sender, KeyboardKeyChangeEventArgs e) + { + if (e.Key == KeyboardKey.KeyD) + { + this.playerStateManager.State |= CharacterState.MovingRight; + } + else if (e.Key == KeyboardKey.KeyA) + { + this.playerStateManager.State |= CharacterState.MovingLeft; + } + else if (e.Key == KeyboardKey.Space) + { + this.playerStateManager.State |= CharacterState.Jumping; + } + else if (e.Key == KeyboardKey.ShiftLeft) + { + this.playerStateManager.State |= CharacterState.Running; + } + } - } + private void KeyboardManagerOnKeyUp(object? sender, KeyboardKeyChangeEventArgs e) + { + if (e.Key == KeyboardKey.KeyD) + { + this.playerStateManager.State ^= CharacterState.MovingRight; + } + else if (e.Key == KeyboardKey.KeyA) + { + this.playerStateManager.State ^= CharacterState.MovingLeft; + } + else if (e.Key == KeyboardKey.Space) + { + this.playerStateManager.State ^= CharacterState.Jumping; + } + else if (e.Key == KeyboardKey.ShiftLeft) + { + this.playerStateManager.State ^= CharacterState.Running; + } + } - void OnGameViewEndInteraction(object sender, TouchEventArgs e) + private void OnGameControllerConnected(object? sender, GameControllerConnectedEventArgs e) + { + if (this.gameController is not null) + { + return; + } + + this.gameController = e.GameController; + + this.gameController.ButtonChanged += GameControllerOnButtonChanged; + this.gameController.ValueChanged += GameControllerOnValueChanged; + } + + private void GameControllerOnValueChanged(object? sender, GameControllerValueChangedEventArgs e) + { + if (gameController is null) + { + return; + } + + if (e.ButtonName == gameController.LeftStick.XAxis.Name) + { + if (e.Value == 0) + { + this.playerStateManager.State = CharacterState.Idle; + } + else if (e.Value < -0.001f) + { + this.playerStateManager.State = CharacterState.MovingLeft; + } + else if (e.Value > 0.001f) + { + this.playerStateManager.State = CharacterState.MovingRight; + } + else + { + this.playerStateManager.State = CharacterState.Idle; + } + + Debug.WriteLine($"e.Value = {e.Value} {this.playerStateManager.State}"); + } + } + + private void GameControllerOnButtonChanged(object? sender, GameControllerButtonChangedEventArgs e) + { + if (gameController is null) + { + return; + } + + if (e.ButtonName == gameController.South.Name) + { + if (e.IsPressed) + { + this.playerStateManager.State |= CharacterState.Jumping; + } + else + { + this.playerStateManager.State ^= CharacterState.Jumping; + } + } + else if (e.ButtonName == gameController.West.Name) + { + if (e.IsPressed) + { + this.playerStateManager.State |= CharacterState.Running; + } + else + { + this.playerStateManager.State ^= CharacterState.Running; + } + } + } + + void OnGameViewEndInteraction(object sender, TouchEventArgs e) { } void OnGameViewStartInteraction(object sender, TouchEventArgs e) { } - + private void OnJumpButtonPressed(object? sender, EventArgs e) { this.playerStateManager.State = CharacterState.Jumping; @@ -54,12 +173,12 @@ private void OnLeftButtonPressed(object? sender, EventArgs e) { this.playerStateManager.State = CharacterState.MovingLeft; } - + private void OnRightButtonPressed(object? sender, EventArgs e) { this.playerStateManager.State = CharacterState.MovingRight; } - + private void OnJumpButtonReleased(object? sender, EventArgs e) { this.playerStateManager.State = CharacterState.Idle; @@ -69,7 +188,7 @@ private void OnLeftButtonReleased(object? sender, EventArgs e) { this.playerStateManager.State = CharacterState.Idle; } - + private void OnRightButtonReleased(object? sender, EventArgs e) { this.playerStateManager.State = CharacterState.Idle; diff --git a/games/Platformer/MauiProgram.cs b/games/Platformer/MauiProgram.cs index ae2c488..7935e6d 100644 --- a/games/Platformer/MauiProgram.cs +++ b/games/Platformer/MauiProgram.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; using Orbit.Engine; +using Orbit.Input; + using Platformer.GameObjects; using Platformer.GameScenes; @@ -13,11 +15,20 @@ public static MauiApp CreateMauiApp() builder .UseMauiApp() .UseOrbitEngine() + .UseOrbitInput(controllerOptions => + { +#if ANDROID + controllerOptions.AutoAttachToLifecycleEvents = true; +#elif WINDOWS + controllerOptions.ControllerUpdateFrequency = TimeSpan.FromMilliseconds(16); +#endif + }) .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); + #if DEBUG builder.Logging.AddDebug(); @@ -29,7 +40,8 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(GameControllerManager.Current); + builder.Services.AddSingleton(KeyboardManager.Current); return builder.Build(); } diff --git a/games/Platformer/Platformer.csproj b/games/Platformer/Platformer.csproj index b7f942f..ef2165b 100644 --- a/games/Platformer/Platformer.csproj +++ b/games/Platformer/Platformer.csproj @@ -1,10 +1,10 @@  - net8.0-android;net8.0-ios;net8.0-maccatalyst - $(TargetFrameworks);net8.0-windows10.0.19041.0 + net9.0-android;net9.0-ios;net9.0-maccatalyst + $(TargetFrameworks);net9.0-windows10.0.19041.0 - +