H073.InputKit
1.0.0
Prefix Reserved
See the version list below for details.
dotnet add package H073.InputKit --version 1.0.0
NuGet\Install-Package H073.InputKit -Version 1.0.0
<PackageReference Include="H073.InputKit" Version="1.0.0" />
<PackageVersion Include="H073.InputKit" Version="1.0.0" />
<PackageReference Include="H073.InputKit" />
paket add H073.InputKit --version 1.0.0
#r "nuget: H073.InputKit, 1.0.0"
#:package H073.InputKit@1.0.0
#addin nuget:?package=H073.InputKit&version=1.0.0
#tool nuget:?package=H073.InputKit&version=1.0.0
Note: This README was generated with the help of AI. The library code itself is entirely human-written and AI-free.
H073.InputKit
A layered, consumption-based input management system for MonoGame. InputKit gives you a clean, structured way to handle keyboard, mouse, gamepad, touch, and text input across multiple layers with automatic input consumption, middleware pipelines, and input sinks.
Features
- Consumption-based input — Higher-priority layers consume input, preventing lower layers from seeing it
- Middleware pipeline — ASP.NET Core-style
Execute(frame, next)middleware for preprocessing input (input buffering, combo detection, dead zones, etc.) - Input sinks — Block all input propagation below a certain layer (useful for modals, pause menus)
- Multi-device support — Keyboard, mouse, gamepad (4 players), touch, and text input
- Data bag — Middleware can attach arbitrary typed data to frames for consumers to read
- Peek & Raw views — Inspect input state without consuming it, or bypass consumption entirely
- Cursor management — Lock, confine, and hide the OS cursor
- Provider abstraction — Swap out MonoGame input for custom/test providers
Installation
dotnet add package H073.InputKit
Quick Start
using InputKit.Core;
using InputKit.Provider;
public class MyGame : Game
{
private InputManager _input;
protected override void Initialize()
{
_input = new InputManager(new MonoGameInputProvider(Window, GraphicsDevice));
// Register a consumer at layer 0
_input.Register(new PlayerController(), layer: 0);
base.Initialize();
}
protected override void Update(GameTime gameTime)
{
_input.Update(gameTime);
base.Update(gameTime);
}
}
A consumer implements IInputConsumer:
using InputKit.Core;
using Microsoft.Xna.Framework.Input;
public class PlayerController : IInputConsumer
{
public bool IsInputEnabled => true;
public void ProcessInput(InputFrame frame)
{
if (frame.WasKeyPressed(Keys.Space))
Jump();
if (frame.IsKeyDown(Keys.A))
MoveLeft();
if (frame.WasMouseButtonPressed(MouseButton.Left))
Attack(frame.MousePosition);
}
}
Core Concepts
Layers & Consumption
Consumers are registered at numeric layers. Higher layers process input first. When a consumer reads input through a consuming method (e.g. WasKeyPressed), that input is marked as consumed at that layer. Lower layers will see false for the same input.
// Layer 100 = UI (highest priority)
// Layer 50 = HUD
// Layer 0 = Gameplay (lowest priority)
_input.Register(uiPanel, layer: 100);
_input.Register(hudController, layer: 50);
_input.Register(playerController, layer: 0);
If the UI panel consumes Keys.Escape at layer 100, neither the HUD nor the player controller will see it.
Consuming vs. Non-Consuming Access
InputFrame provides three ways to read input:
public void ProcessInput(InputFrame frame)
{
// 1. Consuming (default) — marks input as consumed at your layer
bool pressed = frame.WasKeyPressed(Keys.Space);
// 2. Peek — sees all input INCLUDING what higher layers consumed
// Does NOT consume anything itself.
bool peeked = frame.Peek.WasKeyPressed(Keys.Space);
// 3. Raw — ignores consumption entirely, sees true hardware state
// Does NOT consume anything itself.
bool raw = frame.Raw.WasKeyPressed(Keys.Space);
}
When to use each:
- Consuming (default): Normal gameplay — "I want to use this input and prevent others from seeing it"
- Peek: Debugging, analytics, or visual feedback — "I want to know what happened but not interfere"
- Raw: Middleware or global hotkeys — "I need the true hardware state regardless of consumption"
Input Sinks
An InputSink blocks all input propagation below its layer. Useful for modal dialogs, pause menus, or loading screens.
var pauseSink = new InputSink("PauseMenu");
_input.RegisterSink(pauseSink, layer: 90);
// When the pause menu opens:
pauseSink.IsEnabled = true;
// Now all consumers below layer 90 receive no input at all.
// When the pause menu closes:
pauseSink.IsEnabled = false;
// Input flows normally again.
Bulk Consumption
Sometimes you want to consume entire categories of input at once:
public void ProcessInput(InputFrame frame)
{
// Consume ALL keyboard input at this layer
frame.ConsumeAllKeyboard();
// Consume all keyboard input EXCEPT certain keys
frame.ConsumeAllKeyboard(except: Keys.Escape, Keys.F1);
// Consume keyboard input matching a predicate
frame.ConsumeKeyboardWhere(key => key >= Keys.A && key <= Keys.Z);
// Consume all mouse input (buttons, delta, scroll)
frame.ConsumeAllMouse();
// Consume text input characters
frame.ConsumeTextInput();
// Consume touch input
frame.ConsumeTouch();
// Nuclear option: consume everything
frame.ConsumeAll();
}
Modifier Keys
Check modifier state without consuming:
public void ProcessInput(InputFrame frame)
{
if (frame.Modifiers.Ctrl && frame.WasKeyPressed(Keys.S))
Save();
if (frame.Modifiers.Shift && frame.WasKeyPressed(Keys.Z))
Redo();
// Distinguish left/right modifiers
if (frame.Modifiers.LeftAlt)
DoSomething();
}
Middleware
Middleware processes the InputFrame before it reaches any consumer. This is the right place for input buffering, combo detection, dead zone filtering, or any preprocessing.
Writing Middleware
using InputKit.Core;
public class MyMiddleware : IInputMiddleware
{
public int Priority => 0; // Higher = executes first
public bool IsEnabled => true;
public void Execute(InputFrame frame, Action next)
{
// Pre-processing: inspect or modify frame state
next(); // Pass to the next middleware (or consumers)
// Post-processing: react to what consumers did
}
}
Register it:
_input.Use(new MyMiddleware());
Example: Input Buffer
An input buffer remembers recent key presses so that actions can be triggered slightly after the key was pressed (common in platformers for jump buffering).
public class InputBuffer : IInputMiddleware
{
public int Priority => 100;
public bool IsEnabled => true;
private readonly Dictionary<Keys, float> _buffer = new();
private readonly float _bufferDuration;
private float _deltaTime;
public InputBuffer(float bufferDurationSeconds = 0.15f)
{
_bufferDuration = bufferDurationSeconds;
}
public void Execute(InputFrame frame, Action next)
{
_deltaTime = /* get from GameTime or frame data */;
// Record fresh presses
foreach (Keys key in Enum.GetValues<Keys>())
{
if (frame.Raw.WasKeyPressed(key))
_buffer[key] = _bufferDuration;
}
// Tick down timers
var expired = new List<Keys>();
foreach (var (key, time) in _buffer)
{
_buffer[key] = time - _deltaTime;
if (_buffer[key] <= 0)
expired.Add(key);
}
foreach (var key in expired)
_buffer.Remove(key);
// Attach data for consumers to read
frame.SetData(new InputBufferData(this));
next();
}
public bool IsBuffered(Keys key)
=> _buffer.ContainsKey(key) && _buffer[key] > 0;
public void ClearBuffer(Keys key)
=> _buffer.Remove(key);
}
public class InputBufferData
{
private readonly InputBuffer _buffer;
internal InputBufferData(InputBuffer buffer) => _buffer = buffer;
public bool IsBuffered(Keys key) => _buffer.IsBuffered(key);
public void Clear(Keys key) => _buffer.ClearBuffer(key);
}
Consumers read the buffer via the data bag:
public void ProcessInput(InputFrame frame)
{
var buffer = frame.GetData<InputBufferData>();
// Jump if Space was pressed recently (even a few frames ago)
if (buffer?.IsBuffered(Keys.Space) == true && _isGrounded)
{
Jump();
buffer.Clear(Keys.Space); // Don't re-trigger
}
}
Example: Combo Detection
Detect input sequences like fighting game combos or cheat codes.
public class ComboDetector : IInputMiddleware
{
public int Priority => 90;
public bool IsEnabled => true;
private readonly List<Keys> _history = new();
private float _comboTimer;
private readonly float _comboWindow;
public ComboDetector(float comboWindowSeconds = 0.5f)
{
_comboWindow = comboWindowSeconds;
}
public void Execute(InputFrame frame, Action next)
{
// Track pressed keys
foreach (Keys key in Enum.GetValues<Keys>())
{
if (frame.Raw.WasKeyPressed(key))
{
_history.Add(key);
_comboTimer = _comboWindow;
}
}
// Expire old input
_comboTimer -= 0.016f; // or get real delta from frame data
if (_comboTimer <= 0)
_history.Clear();
// Check for combos and publish results
var combos = new ComboData();
if (MatchesSequence(Keys.Up, Keys.Up, Keys.Down, Keys.Down))
{
combos.TriggeredCombos.Add("KonamiStart");
_history.Clear();
}
if (MatchesSequence(Keys.A, Keys.B, Keys.A))
{
combos.TriggeredCombos.Add("SpecialAttack");
_history.Clear();
}
frame.SetData(combos);
next();
}
private bool MatchesSequence(params Keys[] sequence)
{
if (_history.Count < sequence.Length) return false;
var recent = _history.Skip(_history.Count - sequence.Length).ToArray();
return recent.SequenceEqual(sequence);
}
}
public class ComboData
{
public List<string> TriggeredCombos { get; } = new();
}
Consumer side:
public void ProcessInput(InputFrame frame)
{
var combos = frame.GetData<ComboData>();
if (combos != null)
{
foreach (var combo in combos.TriggeredCombos)
{
switch (combo)
{
case "SpecialAttack": DoSpecialAttack(); break;
case "KonamiStart": UnlockSecret(); break;
}
}
}
}
Example: Gamepad Dead Zone
Filter out stick drift by applying a dead zone in middleware:
public class DeadZoneMiddleware : IInputMiddleware
{
public int Priority => 50;
public bool IsEnabled => true;
private readonly float _threshold;
public DeadZoneMiddleware(float threshold = 0.15f)
{
_threshold = threshold;
}
public void Execute(InputFrame frame, Action next)
{
// Read raw axis values and apply dead zone
float lx = frame.Raw.GetGamepadAxis(GamepadAxis.LeftStickX, PlayerIndex.One);
float ly = frame.Raw.GetGamepadAxis(GamepadAxis.LeftStickY, PlayerIndex.One);
var filtered = new DeadZoneData
{
LeftStick = ApplyDeadZone(lx, ly)
};
frame.SetData(filtered);
next();
}
private Vector2 ApplyDeadZone(float x, float y)
{
var v = new Vector2(x, y);
float magnitude = v.Length();
if (magnitude < _threshold) return Vector2.Zero;
// Rescale so edge of dead zone maps to 0
float scaled = (magnitude - _threshold) / (1f - _threshold);
return v * (scaled / magnitude);
}
}
public class DeadZoneData
{
public Vector2 LeftStick { get; init; }
}
Middleware Priority
Middleware executes in descending priority order (highest first). This lets you control the order of preprocessing:
_input.Use(new InputBuffer { /* Priority = 100 */ }); // First: record inputs
_input.Use(new ComboDetector { /* Priority = 90 */ }); // Second: detect combos
_input.Use(new DeadZoneMiddleware { /* Priority = 50 */ }); // Third: filter axes
Mouse Input
public void ProcessInput(InputFrame frame)
{
// Button state (consuming)
if (frame.WasMouseButtonPressed(MouseButton.Left))
OnClick(frame.MousePosition);
if (frame.IsMouseButtonDown(MouseButton.Right))
DrawSelectionBox(frame.MousePosition);
if (frame.WasMouseButtonReleased(MouseButton.Middle))
StopPanning();
// Movement delta (consuming)
Vector2 delta = frame.GetMouseDelta();
RotateCamera(delta);
// Scroll wheel (consuming)
int scroll = frame.GetScrollDelta();
ZoomCamera(scroll);
// Position is always available (non-consuming, read-only)
Vector2 pos = frame.MousePosition;
}
Cursor Management
If your provider implements ICursorManager (the built-in MonoGameInputProvider does):
// Access through InputManager
_input.Cursor.IsVisible = false; // Hide OS cursor
_input.Cursor.IsLocked = true; // Lock to window center (FPS-style)
_input.Cursor.IsConfined = true; // Confine to window bounds
Gamepad Input
InputKit supports up to 4 gamepads via PlayerIndex:
public void ProcessInput(InputFrame frame)
{
if (!frame.IsGamepadConnected(PlayerIndex.One))
return;
// Buttons
if (frame.WasGamepadButtonPressed(Buttons.A, PlayerIndex.One))
Jump();
if (frame.IsGamepadButtonDown(Buttons.RightTrigger, PlayerIndex.One))
Accelerate();
// Axes
float stickX = frame.GetGamepadAxis(GamepadAxis.LeftStickX, PlayerIndex.One);
float stickY = frame.GetGamepadAxis(GamepadAxis.LeftStickY, PlayerIndex.One);
Move(new Vector2(stickX, stickY));
float triggerR = frame.GetGamepadAxis(GamepadAxis.RightTrigger, PlayerIndex.One);
}
Local Multiplayer
_input.Register(new PlayerController(PlayerIndex.One), layer: 0);
_input.Register(new PlayerController(PlayerIndex.Two), layer: 0);
_input.Register(new PlayerController(PlayerIndex.Three), layer: 0);
_input.Register(new PlayerController(PlayerIndex.Four), layer: 0);
Each controller queries its own PlayerIndex, so gamepad input is naturally isolated per player.
Touch Input
public void ProcessInput(InputFrame frame)
{
var touches = frame.GetTouches();
foreach (var touch in touches)
{
switch (touch.State)
{
case TouchLocationState.Pressed:
OnTouchDown(touch.Position);
break;
case TouchLocationState.Moved:
OnTouchMove(touch.Position);
break;
case TouchLocationState.Released:
OnTouchUp(touch.Position);
break;
}
}
}
Text Input
Text input is separate from keyboard key states. It captures the actual characters the user types (respecting OS keyboard layout, IME, etc.):
public void ProcessInput(InputFrame frame)
{
var chars = frame.GetTextInput();
foreach (char c in chars)
{
if (c == '\b')
DeleteLastCharacter();
else
AppendCharacter(c);
}
}
Custom Input Channels
InputKit's InputChannel supports a Custom kind for application-specific input concepts. This is useful when you want to define virtual inputs that don't map directly to hardware.
// Define custom channel IDs
static class VirtualInput
{
// Custom(kind, id) — both are ints you define
public static readonly InputChannel Jump = InputChannel.Custom(1, 0);
public static readonly InputChannel Attack = InputChannel.Custom(1, 1);
public static readonly InputChannel Interact = InputChannel.Custom(1, 2);
public static readonly InputChannel OpenMenu = InputChannel.Custom(2, 0);
}
You can consume and query custom channels through the generic consumption API:
public void ProcessInput(InputFrame frame)
{
// Check if a custom channel was already consumed by a higher layer
if (!frame.IsConsumedAbove(VirtualInput.Jump))
{
frame.Consume(VirtualInput.Jump);
Jump();
}
}
A middleware can map hardware input to custom channels and publish the mapping via the data bag, creating a full input remapping system.
Custom Providers
You can swap out the MonoGame provider entirely — useful for testing, replays, or alternative input sources.
Implement one or more provider interfaces:
public class ReplayProvider : IKeyboardProvider, IMouseProvider
{
private readonly Queue<KeyboardSnapshot> _keyboardFrames;
private readonly Queue<MouseSnapshot> _mouseFrames;
public void Poll() { /* advance to next frame */ }
public KeyboardSnapshot GetKeyboardState()
=> _keyboardFrames.Dequeue();
public MouseSnapshot GetMouseState()
=> _mouseFrames.Dequeue();
}
Register it:
var replay = new ReplayProvider(recordedData);
var input = new InputManager(replay);
// or:
var input = new InputManager();
input.AddProvider(replay);
AddProvider(object) auto-detects which interfaces the provider implements and registers each one.
You can also register providers individually:
_input.UseKeyboard(myKeyboardProvider);
_input.UseMouse(myMouseProvider);
_input.UseGamepad(myGamepadProvider);
Debugging
InputKit includes built-in debug tools:
// Overview of registered consumers, middleware, and sinks
string overview = _input.GetDebugOverview();
// Per-frame consumption report (what was consumed, by whom, at which layer)
string report = _input.GetFrameReport();
Full Example: Game with UI Layer
public class MyGame : Game
{
private InputManager _input;
private InputSink _modalSink;
protected override void Initialize()
{
var provider = new MonoGameInputProvider(Window, GraphicsDevice);
_input = new InputManager(provider);
// Middleware
_input.Use(new InputBuffer(0.12f));
_input.Use(new DeadZoneMiddleware(0.2f));
// Consumers (high layer = high priority)
_input.Register(new ModalDialog(), layer: 200);
_input.Register(new UISystem(), layer: 100);
_input.Register(new PlayerControl(), layer: 0);
// Sink for blocking input during modals
_modalSink = new InputSink("ModalBlocker");
_input.RegisterSink(_modalSink, layer: 150);
base.Initialize();
}
public void ShowModal()
{
_modalSink.IsEnabled = true;
// Now only layer 200 (ModalDialog) receives input.
// Layer 100 (UISystem) and 0 (PlayerControl) are blocked.
}
public void HideModal()
{
_modalSink.IsEnabled = false;
// All layers receive input again.
}
protected override void Update(GameTime gameTime)
{
_input.Update(gameTime);
base.Update(gameTime);
}
}
API Reference Summary
| Type | Description |
|---|---|
InputManager |
Main orchestrator — registers providers, middleware, consumers |
InputFrame |
Per-frame input state with consuming methods |
InputFrameView |
Non-consuming view (Peek or Raw) |
IInputConsumer |
Interface for layer-based input consumers |
IInputMiddleware |
ASP.NET Core-style middleware interface |
InputSink |
Blocks input propagation below its layer |
InputChannel |
Generic channel identifier (Key, Mouse, Gamepad, Touch, Custom) |
InputModifiers |
Ctrl/Shift/Alt modifier state |
ICursorManager |
Cursor visibility, lock, and confinement |
MonoGameInputProvider |
Built-in provider for MonoGame |
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MonoGame.Framework.DesktopGL (>= 3.8.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.