Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s
CI / build-test (push) Successful in 1m6s
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
|
||||
namespace MrGameEng.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Maps game actions (an enum) to any number of physical bindings: keys, mouse buttons or
|
||||
/// gamepad buttons. Query by action instead of device, rebind at runtime.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">Enum (or any value) identifying the game's actions.</typeparam>
|
||||
public sealed class ActionMap<TAction> where TAction : notnull
|
||||
{
|
||||
private readonly InputManager _input;
|
||||
private readonly Dictionary<TAction, List<Binding>> _bindings = new();
|
||||
|
||||
private readonly record struct Binding(Keys? Key, MouseButton? Mouse, Buttons? GamePad);
|
||||
|
||||
/// <summary>Creates an action map querying <paramref name="input"/>.</summary>
|
||||
public ActionMap(InputManager input) => _input = input;
|
||||
|
||||
/// <summary>Adds a keyboard binding for <paramref name="action"/>.</summary>
|
||||
public ActionMap<TAction> Bind(TAction action, Keys key) => Add(action, new Binding(key, null, null));
|
||||
|
||||
/// <summary>Adds a mouse-button binding for <paramref name="action"/>.</summary>
|
||||
public ActionMap<TAction> Bind(TAction action, MouseButton button) => Add(action, new Binding(null, button, null));
|
||||
|
||||
/// <summary>Adds a gamepad-button binding for <paramref name="action"/>.</summary>
|
||||
public ActionMap<TAction> Bind(TAction action, Buttons button) => Add(action, new Binding(null, null, button));
|
||||
|
||||
/// <summary>Removes every binding of <paramref name="action"/> (for rebinding).</summary>
|
||||
public void Unbind(TAction action) => _bindings.Remove(action);
|
||||
|
||||
/// <summary>True while any binding of the action is held down.</summary>
|
||||
public bool IsDown(TAction action) => Any(action,
|
||||
static (input, b) =>
|
||||
(b.Key is { } k && input.IsKeyDown(k)) ||
|
||||
(b.Mouse is { } m && input.IsMouseDown(m)) ||
|
||||
(b.GamePad is { } g && input.IsButtonDown(g)));
|
||||
|
||||
/// <summary>True only on the frame any binding of the action went down.</summary>
|
||||
public bool IsPressed(TAction action) => Any(action,
|
||||
static (input, b) =>
|
||||
(b.Key is { } k && input.IsKeyPressed(k)) ||
|
||||
(b.Mouse is { } m && input.IsMousePressed(m)) ||
|
||||
(b.GamePad is { } g && input.IsButtonPressed(g)));
|
||||
|
||||
/// <summary>True only on the frame any binding of the action went up.</summary>
|
||||
public bool IsReleased(TAction action) => Any(action,
|
||||
static (input, b) =>
|
||||
(b.Key is { } k && input.IsKeyReleased(k)) ||
|
||||
(b.Mouse is { } m && input.IsMouseReleased(m)) ||
|
||||
(b.GamePad is { } g && input.IsButtonReleased(g)));
|
||||
|
||||
/// <summary>Composes -1/0/+1 from two digital actions (e.g. move left / move right).</summary>
|
||||
public float GetAxis(TAction negative, TAction positive) =>
|
||||
(IsDown(positive) ? 1f : 0f) - (IsDown(negative) ? 1f : 0f);
|
||||
|
||||
private ActionMap<TAction> Add(TAction action, Binding binding)
|
||||
{
|
||||
if (!_bindings.TryGetValue(action, out var list))
|
||||
{
|
||||
list = [];
|
||||
_bindings.Add(action, list);
|
||||
}
|
||||
|
||||
list.Add(binding);
|
||||
return this;
|
||||
}
|
||||
|
||||
private bool Any(TAction action, Func<InputManager, Binding, bool> predicate)
|
||||
{
|
||||
if (!_bindings.TryGetValue(action, out var list))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var binding in list)
|
||||
{
|
||||
if (predicate(_input, binding))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
|
||||
namespace MrGameEng.Input;
|
||||
|
||||
/// <summary>Mouse buttons addressable through <see cref="InputManager"/>.</summary>
|
||||
public enum MouseButton
|
||||
{
|
||||
/// <summary>Left button.</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Right button.</summary>
|
||||
Right,
|
||||
|
||||
/// <summary>Middle button (wheel click).</summary>
|
||||
Middle,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls keyboard, mouse and gamepad once per frame and keeps the previous frame's state,
|
||||
/// enabling edge queries (<c>Pressed</c> = went down this frame, <c>Released</c> = went up).
|
||||
/// Registered as a service by <c>scene.UseInput()</c>; polled by <see cref="InputSystem"/>
|
||||
/// at the start of the update phase.
|
||||
/// </summary>
|
||||
public sealed class InputManager
|
||||
{
|
||||
private KeyboardState _keyboard;
|
||||
private KeyboardState _previousKeyboard;
|
||||
private MouseState _mouse;
|
||||
private MouseState _previousMouse;
|
||||
private GamePadState _gamePad;
|
||||
private GamePadState _previousGamePad;
|
||||
|
||||
/// <summary>Polls all devices. Called once per frame by <see cref="InputSystem"/>.</summary>
|
||||
public void Update() => Apply(
|
||||
Keyboard.GetState(),
|
||||
Mouse.GetState(),
|
||||
GamePad.GetState(PlayerIndex.One));
|
||||
|
||||
internal void Apply(KeyboardState keyboard, MouseState mouse, GamePadState gamePad)
|
||||
{
|
||||
_previousKeyboard = _keyboard;
|
||||
_previousMouse = _mouse;
|
||||
_previousGamePad = _gamePad;
|
||||
_keyboard = keyboard;
|
||||
_mouse = mouse;
|
||||
_gamePad = gamePad;
|
||||
}
|
||||
|
||||
/// <summary>True while the key is held down.</summary>
|
||||
public bool IsKeyDown(Keys key) => _keyboard.IsKeyDown(key);
|
||||
|
||||
/// <summary>True only on the frame the key went down.</summary>
|
||||
public bool IsKeyPressed(Keys key) => _keyboard.IsKeyDown(key) && _previousKeyboard.IsKeyUp(key);
|
||||
|
||||
/// <summary>True only on the frame the key went up.</summary>
|
||||
public bool IsKeyReleased(Keys key) => _keyboard.IsKeyUp(key) && _previousKeyboard.IsKeyDown(key);
|
||||
|
||||
/// <summary>Mouse cursor position in window pixels.</summary>
|
||||
public Point MousePosition => _mouse.Position;
|
||||
|
||||
/// <summary>Cursor movement since the previous frame.</summary>
|
||||
public Point MouseDelta => _mouse.Position - _previousMouse.Position;
|
||||
|
||||
/// <summary>Scroll wheel change since the previous frame (positive = up).</summary>
|
||||
public int WheelDelta => _mouse.ScrollWheelValue - _previousMouse.ScrollWheelValue;
|
||||
|
||||
/// <summary>True while the mouse button is held down.</summary>
|
||||
public bool IsMouseDown(MouseButton button) => GetButton(_mouse, button) == ButtonState.Pressed;
|
||||
|
||||
/// <summary>True only on the frame the mouse button went down.</summary>
|
||||
public bool IsMousePressed(MouseButton button) =>
|
||||
GetButton(_mouse, button) == ButtonState.Pressed && GetButton(_previousMouse, button) == ButtonState.Released;
|
||||
|
||||
/// <summary>True only on the frame the mouse button went up.</summary>
|
||||
public bool IsMouseReleased(MouseButton button) =>
|
||||
GetButton(_mouse, button) == ButtonState.Released && GetButton(_previousMouse, button) == ButtonState.Pressed;
|
||||
|
||||
/// <summary>True while the gamepad button is held down.</summary>
|
||||
public bool IsButtonDown(Buttons button) => _gamePad.IsButtonDown(button);
|
||||
|
||||
/// <summary>True only on the frame the gamepad button went down.</summary>
|
||||
public bool IsButtonPressed(Buttons button) => _gamePad.IsButtonDown(button) && _previousGamePad.IsButtonUp(button);
|
||||
|
||||
/// <summary>True only on the frame the gamepad button went up.</summary>
|
||||
public bool IsButtonReleased(Buttons button) => _gamePad.IsButtonUp(button) && _previousGamePad.IsButtonDown(button);
|
||||
|
||||
/// <summary>Left thumbstick, x/y in [-1, 1]. Y is inverted to match the engine's y-down world.</summary>
|
||||
public Vector2 LeftStick => new(_gamePad.ThumbSticks.Left.X, -_gamePad.ThumbSticks.Left.Y);
|
||||
|
||||
private static ButtonState GetButton(in MouseState state, MouseButton button) => button switch
|
||||
{
|
||||
MouseButton.Left => state.LeftButton,
|
||||
MouseButton.Right => state.RightButton,
|
||||
MouseButton.Middle => state.MiddleButton,
|
||||
_ => ButtonState.Released,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace MrGameEng.Input;
|
||||
|
||||
/// <summary>Polls the <see cref="InputManager"/> once per frame. Registered first in the update phase.</summary>
|
||||
public sealed class InputSystem : BaseSystem
|
||||
{
|
||||
private readonly InputManager _input;
|
||||
|
||||
/// <summary>Creates the system for <paramref name="input"/>.</summary>
|
||||
public InputSystem(InputManager input) => _input = input;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnUpdateGroup() => _input.Update();
|
||||
}
|
||||
|
||||
/// <summary>Wires the input module into a <see cref="Scene"/>.</summary>
|
||||
public static class SceneInputExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the shared <see cref="InputManager"/> service (creating it on first use) and
|
||||
/// inserts <see cref="InputSystem"/> at the start of the scene's update phase.
|
||||
/// Call from <c>OnLoad</c> before adding gameplay systems.
|
||||
/// </summary>
|
||||
public static InputManager UseInput(this Scene scene)
|
||||
{
|
||||
var services = scene.Context.Services;
|
||||
var input = services.GetOrDefault<InputManager>();
|
||||
if (input is null)
|
||||
{
|
||||
input = new InputManager();
|
||||
services.Add(input);
|
||||
}
|
||||
|
||||
scene.UpdateSystems.Insert(0, new InputSystem(input));
|
||||
return input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="MrGameEng.Input.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user