H073.InputKit
1.1.0
Prefix Reserved
dotnet add package H073.InputKit --version 1.1.0
NuGet\Install-Package H073.InputKit -Version 1.1.0
<PackageReference Include="H073.InputKit" Version="1.1.0" />
<PackageVersion Include="H073.InputKit" Version="1.1.0" />
<PackageReference Include="H073.InputKit" />
paket add H073.InputKit --version 1.1.0
#r "nuget: H073.InputKit, 1.1.0"
#:package H073.InputKit@1.1.0
#addin nuget:?package=H073.InputKit&version=1.1.0
#tool nuget:?package=H073.InputKit&version=1.1.0
H073.InputKit
A layered, consumption-based input system for MonoGame. InputKit gives you a structured way to handle keyboard, mouse, gamepad, touch, and text input across prioritized layers with automatic input consumption, pre/post-dispatch callbacks, built-in trackers, and input sinks.
dotnet add package H073.InputKit
Requirements: MonoGame 3.8+ | .NET 10+
Table of Contents
- Architecture Overview
- Quick Start
- The Update Cycle
- Layers and Consumers
- Input Consumption
- Input Sinks
- Pre/Post-Dispatch Callbacks
- The Data Bag
- Built-in Trackers
- Frame Identity and Timing
- Manager-Level Data Access
- Input Channels
- Keyboard Input
- Mouse Input
- Modifier Keys
- Gamepad Input
- Touch Input
- Text Input
- Cursor Management
- Custom Providers
- Debugging
- Middleware (Legacy)
- API Reference
Architecture Overview
InputKit is organized into four namespaces:
InputKit.Core — InputManager, InputFrame, InputFrameView, InputChannel,
InputModifiers, InputSink, IInputConsumer, ICursorManager
InputKit.State — KeyboardSnapshot, MouseSnapshot, GamepadSnapshot,
TextInputSnapshot, TouchSnapshot, MouseButton, GamepadAxis
InputKit.Trackers — HoldTracker, HoldData, DoubleTapTracker, TapData
InputKit.Provider — MonoGameInputProvider
The core design principles:
- Layer-based consumption — Higher layers process input first and can consume it, blocking lower layers.
- Zero-opinion extensibility — Pre/post-dispatch callbacks let you build any input feature (combos, hold timers, gesture recognition, replay, analytics) without subclassing or framework constraints.
- No per-frame allocation — Snapshots are readonly structs holding only current + previous hardware state.
- Thread-safe text input — OS text input events are collected via
ConcurrentQueue<char>and drained into a reusable list each frame. - Framework-agnostic core — Provider interfaces abstract hardware access. Swap
MonoGameInputProviderfor test stubs, replay systems, or other frameworks.
Quick Start
using InputKit.Core;
using InputKit.Provider;
public class MyGame : Game
{
private InputManager _input;
protected override void Initialize()
{
var provider = new MonoGameInputProvider(this);
_input = new InputManager(provider);
_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 InputKit.State;
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);
}
}
The Update Cycle
Every call to InputManager.Update(gameTime) runs exactly this sequence:
1. POLL — All registered providers call Poll() (deduplicated by object identity)
2. SNAPSHOT — Keyboard, mouse, gamepad, text, touch snapshots are captured
3. RESET — ConsumptionTracker is cleared, data bag is emptied
4. MIDDLEWARE — Legacy IInputMiddleware pipeline executes (deprecated, see below)
5. PRE-DISPATCH — Pre-dispatch callbacks run (highest priority first)
6. DISPATCH — Consumers run layer by layer, HIGH → LOW, sinks checked after each layer
7. POST-DISPATCH — Post-dispatch callbacks run (highest priority first)
Pre-dispatch is where you compute derived input data (combos, hold durations, charge bars). Dispatch is where consumers read and consume that data. Post-dispatch is where you do analytics, replay recording, or cleanup.
Layers and Consumers
Consumers are registered at numeric layer indices. Higher numbers = higher priority = process first.
// Layer 200 = Modal dialog (highest priority)
// Layer 100 = UI panel
// Layer 50 = HUD / toolbar
// Layer 0 = Gameplay (lowest priority)
_input.Register(modalDialog, layer: 200);
_input.Register(uiPanel, layer: 100);
_input.Register(toolbar, layer: 50);
_input.Register(playerController, layer: 0);
Multiple consumers can share a layer. Within the same layer, consumers run in registration order.
You can also let InputKit auto-assign layers:
_input.Register(first); // auto-assigned layer 999
_input.Register(second); // auto-assigned layer 998
_input.Register(third); // auto-assigned layer 997
Unregister a consumer at any time:
_input.Unregister(uiPanel);
IInputConsumer Interface
public interface IInputConsumer
{
/// Called once per frame during the DISPATCH phase.
void ProcessInput(InputFrame frame);
/// Return false to temporarily skip this consumer without unregistering.
bool IsInputEnabled { get; }
}
When IsInputEnabled returns false, the consumer is skipped entirely — it does not consume anything and does not see any input.
Input Consumption
Three Access Modes
Every piece of input data can be accessed in three ways:
| Mode | Source | Consumes? | Sees consumed input? | Use case |
|---|---|---|---|---|
| Consuming | InputFrame methods |
Yes (on success) | No | Normal gameplay input |
| Peek | frame.Peek.* |
No | No (respects consumption) | Tooltips, visual feedback |
| Raw | frame.Raw.* |
No | Yes (ignores consumption) | Debug overlays, recording, global hotkeys |
Consuming Methods
These are the primary methods on InputFrame. They follow a single rule: consume on success. If the input is available (not consumed by a higher layer) and the hardware state matches, the method returns a truthy value and marks the channel as consumed. If not, it returns a falsy value and consumes nothing.
Keyboard:
| Method | Returns | Consumes |
|---|---|---|
WasKeyPressed(Keys key) |
bool |
The key channel, if the key was just pressed |
WasKeyReleased(Keys key) |
bool |
The key channel, if the key was just released |
IsKeyDown(Keys key) |
bool |
The key channel, if the key is currently held |
Mouse:
| Method | Returns | Consumes |
|---|---|---|
WasMouseButtonPressed(MouseButton) |
bool |
The button channel |
WasMouseButtonReleased(MouseButton) |
bool |
The button channel |
IsMouseButtonDown(MouseButton) |
bool |
The button channel |
GetMouseDelta() |
Vector2 |
The mouse delta channel, if non-zero |
GetScrollDelta() |
int |
The scroll channel, if non-zero |
Text Input:
| Method | Returns | Consumes |
|---|---|---|
GetTextInput() |
IReadOnlyList<char> |
The text input channel, if non-empty |
Gamepad:
| Method | Returns | Consumes |
|---|---|---|
WasGamepadButtonPressed(Buttons, PlayerIndex) |
bool |
The button channel |
WasGamepadButtonReleased(Buttons, PlayerIndex) |
bool |
The button channel |
IsGamepadButtonDown(Buttons, PlayerIndex) |
bool |
The button channel |
GetGamepadAxis(GamepadAxis, PlayerIndex) |
float |
The axis channel, if non-zero |
Touch:
| Method | Returns | Consumes |
|---|---|---|
GetTouches() |
IReadOnlyList<TouchLocation> |
The touch channel, if non-empty |
Gamepad Connection (non-consuming):
| Method | Returns | Consumes |
|---|---|---|
IsGamepadConnected(PlayerIndex) |
bool |
Never |
Data Bag:
| Method | Returns | Consumes |
|---|---|---|
GetData<T>() |
T? |
The data type, if present |
ConsumeData<T>() |
void | Explicitly consumes a data type |
ConsumeAllData() |
void | Consumes all data bag entries |
Non-Consuming Properties
These properties on InputFrame always return the raw hardware value regardless of consumption. They never consume anything.
frame.MousePosition // Vector2 — current screen-space mouse position
frame.MouseDelta // Vector2 — raw movement delta
frame.ScrollWheelDelta // int — raw scroll delta
frame.TextInput // IReadOnlyList<char> — raw characters
frame.Modifiers // InputModifiers — Ctrl/Shift/Alt state
Peek and Raw Views
InputFrame exposes two InputFrameView properties:
// Peek: respects consumption (won't see input consumed above) but does NOT consume itself.
bool peeked = frame.Peek.WasKeyPressed(Keys.Space);
// Raw: ignores consumption entirely — always returns true hardware state.
bool raw = frame.Raw.WasKeyPressed(Keys.Space);
Both views expose the full set of query methods (keyboard, mouse, gamepad, touch, text, data bag) plus DeltaTime, FrameNumber, TotalTime, Modifiers, MousePosition, GetPressedKeys(), and IsConsumedAbove(InputChannel).
When to use each:
- Consuming (default): "I want this input and no one below me should see it"
- Peek: "I want to observe without interfering" (UI highlights, hover effects)
- Raw: "I need hardware truth regardless of game state" (debug overlays, input recording, global hotkeys)
Bulk Consumption
Consume entire categories at once:
public void ProcessInput(InputFrame frame)
{
// Consume ALL keyboard keys
frame.ConsumeAllKeyboard();
// Consume all keys EXCEPT specific ones
frame.ConsumeAllKeyboard(Keys.Escape, Keys.F1);
// Consume keys matching a predicate
frame.ConsumeKeyboardWhere(key => key >= Keys.A && key <= Keys.Z);
// Consume all mouse channels (buttons + delta + scroll)
frame.ConsumeAllMouse();
// Consume all gamepad channels (all buttons + axes for all 4 players)
frame.ConsumeAllGamepad();
// Consume text input
frame.ConsumeTextInput();
// Consume touch
frame.ConsumeTouch();
// Consume EVERYTHING (keyboard + mouse + text + touch + all data)
frame.ConsumeAll();
}
Input Sinks
An InputSink blocks all input propagation below its layer. When a sink is enabled, the dispatch loop stops after processing that layer — no lower layers run at all.
var pauseSink = new InputSink("PauseMenu");
_input.RegisterSink(pauseSink, layer: 90);
// Open pause menu — layers below 90 receive no input
pauseSink.IsEnabled = true;
// Close pause menu — input flows normally
pauseSink.IsEnabled = false;
Sinks are independent of consumers. You can have a sink on a layer with no consumers, or a layer with consumers and no sink.
_input.UnregisterSink(pauseSink); // Remove entirely
Pre/Post-Dispatch Callbacks
Pre/post-dispatch callbacks are the primary extensibility mechanism. They replace the old middleware system with a simpler, more flexible model.
Pre-Dispatch
Pre-dispatch callbacks run after snapshots are captured but before any consumer processes input. This is where you compute derived input data and write it to the frame's data bag.
_input.OnPreDispatch(frame =>
{
// Read raw input (non-consuming)
if (frame.Raw.WasKeyPressed(Keys.F1))
_debugActive = !_debugActive;
// Write data for consumers to read
frame.SetData(new MyCustomData { IsDebugActive = _debugActive });
}, priority: 10); // Higher priority = runs first
Post-Dispatch
Post-dispatch callbacks run after all consumers have processed. Use them for analytics, replay recording, or cleanup.
_input.OnPostDispatch(frame =>
{
// Log what was consumed this frame
var consumptions = _input.GetConsumptions();
foreach (var (channel, (layer, consumer)) in consumptions)
_logger.Log($"{channel} consumed by {consumer} on layer {layer}");
}, priority: 0);
Priority Ordering
Both pre-dispatch and post-dispatch callbacks are sorted by priority in descending order (higher values run first).
_input.OnPreDispatch(tracker.Process, priority: 100); // Runs first
_input.OnPreDispatch(enricher.Process, priority: 50); // Runs second
_input.OnPreDispatch(validator.Process, priority: 0); // Runs last
Removal
_input.RemovePreDispatch(tracker.Process);
_input.RemovePostDispatch(logger.Process);
Pre-Dispatch vs. Middleware
| Feature | Pre/Post-Dispatch | Legacy Middleware |
|---|---|---|
| Registration | OnPreDispatch(handler, priority) |
Use(IInputMiddleware) |
| Signature | Action<InputFrame> |
Execute(InputFrame, Action next) |
| Chain control | No next() — all callbacks always run |
Must call next() or chain stops |
| Can block pipeline | No | Yes (by not calling next()) |
| Priority ordering | Yes (higher first) | Yes (higher first) |
| Removal | RemovePreDispatch(handler) |
RemoveMiddleware(middleware) |
Pre/post-dispatch is simpler and covers all common use cases. Use it for everything new.
The Data Bag
The data bag is a per-frame, type-keyed Dictionary<Type, object> on InputFrame. It allows pre-dispatch code to write computed data that consumers read.
Writing Data (Pre-Dispatch)
// One entry per type — type is the key
frame.SetData(new ChargeData { Progress = 0.75f });
frame.SetData(new ComboData { TriggeredCombo = "Konami" });
Reading Data (Consumers)
public void ProcessInput(InputFrame frame)
{
// GetData<T>() consumes the data type (lower layers won't see it)
var charge = frame.GetData<ChargeData>();
if (charge?.Progress >= 1.0f)
FireChargedShot();
// Peek.GetData<T>() reads without consuming
var combo = frame.Peek.GetData<ComboData>();
// Raw.GetData<T>() reads ignoring consumption
var debug = frame.Raw.GetData<DebugData>();
}
Data Consumption
Data consumption follows the same layer rules as input channels:
// Explicitly consume a data type without reading it
frame.ConsumeData<ChargeData>();
// Consume all data types
frame.ConsumeAllData();
The data bag is cleared automatically at the start of each frame.
Built-in Trackers
InputKit includes two ready-to-use trackers that register as pre-dispatch callbacks and write their results to the data bag.
HoldTracker
Tracks how long keys and mouse buttons have been continuously held down.
var holdTracker = new HoldTracker();
_input.OnPreDispatch(holdTracker.Process, priority: 100);
Consumer usage:
public void ProcessInput(InputFrame frame)
{
var hold = frame.GetData<HoldData>();
if (hold == null) return;
// Get hold duration in seconds
float duration = hold.GetHoldDuration(Keys.Space);
// Check if held for at least N seconds
if (hold.IsHeldFor(Keys.Space, 2.0f))
ChargedAttack();
// Works for mouse buttons too
if (hold.IsHeldFor(MouseButton.Left, 0.5f))
StartDrag();
}
HoldData API:
| Method | Returns |
|---|---|
GetHoldDuration(Keys key) |
float — seconds held, or 0 if not held |
IsHeldFor(Keys key, float seconds) |
bool — true if held >= seconds |
GetHoldDuration(MouseButton button) |
float — seconds held, or 0 if not held |
IsHeldFor(MouseButton button, float seconds) |
bool — true if held >= seconds |
The tracker reads raw input and is non-consuming — it does not block any layer from seeing the input.
DoubleTapTracker
Detects double-taps (and multi-taps) on keys and mouse buttons within a configurable time window.
var tapTracker = new DoubleTapTracker();
tapTracker.TapWindow = 0.3f; // seconds (default)
tapTracker.MaxDistance = 10f; // pixels (0 = disabled, default)
_input.OnPreDispatch(tapTracker.Process, priority: 90);
Properties:
| Property | Type | Default | Description |
|---|---|---|---|
TapWindow |
float |
0.3 | Maximum seconds between consecutive taps |
MaxDistance |
float |
0 | Maximum pixel distance between mouse taps (0 = disabled) |
Consumer usage:
public void ProcessInput(InputFrame frame)
{
var taps = frame.GetData<TapData>();
if (taps == null) return;
// Check for double-tap
if (taps.WasDoubleTapped(Keys.Space))
Dodge();
// Get exact tap count (1 = single, 2 = double, 3 = triple, ...)
int clickCount = taps.GetTapCount(MouseButton.Left);
if (clickCount == 3)
SelectParagraph();
}
TapData API:
| Method | Returns |
|---|---|
GetTapCount(Keys key) |
int — tap count this frame (0 if not tapped) |
WasDoubleTapped(Keys key) |
bool — true if tap count >= 2 |
GetTapCount(MouseButton button) |
int — tap count this frame (0 if not tapped) |
WasDoubleTapped(MouseButton button) |
bool — true if tap count >= 2 |
Frame Identity and Timing
Every InputFrame and InputFrameView carries timing information:
public void ProcessInput(InputFrame frame)
{
float dt = frame.DeltaTime; // Seconds since last frame (float)
uint fn = frame.FrameNumber; // Monotonically increasing counter
double total = frame.TotalTime; // Total seconds since game start (double)
}
These are also available on frame.Peek and frame.Raw:
float dt = frame.Raw.DeltaTime;
Common uses:
DeltaTime— frame-rate independent movement, timers, cooldownsFrameNumber— replay indexing, deterministic simulation, networkingTotalTime— absolute timestamps for combo timeout windows, debouncing
Manager-Level Data Access
After calling InputManager.Update(), you can query the frame directly from the manager without going through a consumer:
_input.Update(gameTime);
// Read data bag entries written by pre-dispatch callbacks
var holdData = _input.GetData<HoldData>();
var tapData = _input.GetData<TapData>();
// Access the full frame object
InputFrame frame = _input.Frame;
float dt = frame.DeltaTime;
// See which channels were consumed and by whom
var consumptions = _input.GetConsumptions();
foreach (var (channel, (layer, consumer)) in consumptions)
Console.WriteLine($"{channel} -> {consumer} (layer {layer})");
This is useful for rendering code, analytics, or any system that runs outside the consumer dispatch phase.
Input Channels
Built-in Channels
Every input event is identified by an InputChannel — a lightweight value type with three fields: Kind, Id, SubId.
| Factory | Kind | Id | SubId |
|---|---|---|---|
InputChannel.Key(Keys.Space) |
Key | (int)Keys.Space | 0 |
InputChannel.Mouse(MouseButton.Left) |
MouseButton | (int)MouseButton.Left | 0 |
InputChannel.MouseDelta |
MouseDelta | 0 | 0 |
InputChannel.Scroll |
Scroll | 0 | 0 |
InputChannel.TextInput |
TextInput | 0 | 0 |
InputChannel.Gamepad(Buttons.A, PlayerIndex.One) |
GamepadButton | (int)Buttons.A | 0 |
InputChannel.Gamepad(GamepadAxis.LeftStickX, PlayerIndex.Two) |
GamepadAxis | (int)LeftStickX | 1 |
InputChannel.Touch |
Touch | 0 | 0 |
Channels are used internally by the consumption tracker. You typically don't create them manually unless working with custom channels.
Custom Channels
Define application-specific input channels using InputChannel.Custom(id, subId):
static class VirtualInput
{
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);
}
Consume and query custom channels in consumers:
public void ProcessInput(InputFrame frame)
{
if (!frame.IsConsumedAbove(VirtualInput.Jump))
{
frame.Consume(VirtualInput.Jump);
Jump();
}
}
A pre-dispatch callback can map hardware input to custom channels, creating a full input remapping system.
Keyboard Input
public void ProcessInput(InputFrame frame)
{
// Edge detection (consuming)
if (frame.WasKeyPressed(Keys.Space)) // Just pressed this frame
Jump();
if (frame.WasKeyReleased(Keys.Space)) // Just released this frame
StopCharging();
// Hold detection (consuming)
if (frame.IsKeyDown(Keys.W)) // Currently held
MoveForward();
}
Mouse Input
public void ProcessInput(InputFrame frame)
{
// Buttons (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 — returns Vector2.Zero if consumed above)
Vector2 delta = frame.GetMouseDelta();
RotateCamera(delta);
// Scroll wheel (consuming — returns 0 if consumed above)
int scroll = frame.GetScrollDelta();
ZoomCamera(scroll);
// Position (non-consuming property — always available)
Vector2 pos = frame.MousePosition;
}
MouseButton enum values: Left, Right, Middle, XButton1, XButton2.
Modifier Keys
InputModifiers is a readonly struct that reads modifier state from the keyboard snapshot. It is non-consuming — always returns the true hardware state.
public void ProcessInput(InputFrame frame)
{
// Combined (either left or right)
if (frame.Modifiers.Ctrl && frame.WasKeyPressed(Keys.S))
Save();
if (frame.Modifiers.Shift && frame.WasKeyPressed(Keys.Z))
Redo();
if (frame.Modifiers.Alt && frame.WasKeyPressed(Keys.Enter))
ToggleFullscreen();
// Specific side
if (frame.Modifiers.LeftCtrl)
DoLeftCtrlThing();
if (frame.Modifiers.RightAlt)
DoRightAltThing();
}
Properties:
| Property | Type | Description |
|---|---|---|
Ctrl |
bool |
Either Control key |
Shift |
bool |
Either Shift key |
Alt |
bool |
Either Alt key |
LeftCtrl |
bool |
Left Control only |
RightCtrl |
bool |
Right Control only |
LeftShift |
bool |
Left Shift only |
RightShift |
bool |
Right Shift only |
LeftAlt |
bool |
Left Alt only |
RightAlt |
bool |
Right Alt only |
Gamepad Input
InputKit supports up to 4 gamepads via PlayerIndex:
public void ProcessInput(InputFrame frame)
{
if (!frame.IsGamepadConnected(PlayerIndex.One))
return;
// Buttons (consuming)
if (frame.WasGamepadButtonPressed(Buttons.A))
Jump();
if (frame.IsGamepadButtonDown(Buttons.RightTrigger))
Accelerate();
if (frame.WasGamepadButtonReleased(Buttons.X))
ReleaseAbility();
// Analog axes (consuming — returns 0 if consumed above)
float stickX = frame.GetGamepadAxis(GamepadAxis.LeftStickX);
float stickY = frame.GetGamepadAxis(GamepadAxis.LeftStickY);
Move(new Vector2(stickX, stickY));
float trigger = frame.GetGamepadAxis(GamepadAxis.RightTrigger);
}
GamepadAxis values:
| Axis | Range | Description |
|---|---|---|
LeftStickX |
-1 to +1 | Left thumbstick horizontal |
LeftStickY |
-1 to +1 | Left thumbstick vertical |
RightStickX |
-1 to +1 | Right thumbstick horizontal |
RightStickY |
-1 to +1 | Right thumbstick vertical |
LeftTrigger |
0 to 1 | Left trigger |
RightTrigger |
0 to 1 | Right trigger |
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, and dead keys.
public void ProcessInput(InputFrame frame)
{
var chars = frame.GetTextInput();
foreach (char c in chars)
{
if (c == '\b')
DeleteLastCharacter();
else if (c == '\r' || c == '\n')
Submit();
else
AppendCharacter(c);
}
}
Text input events arrive from the OS on potentially different threads. MonoGameInputProvider collects them via a ConcurrentQueue<char> and drains them into a per-frame list during Poll().
Cursor Management
If your provider implements ICursorManager (the built-in MonoGameInputProvider does):
// Access through InputManager.Cursor
_input.Cursor.IsVisible = false; // Hide the OS cursor
_input.Cursor.IsLocked = true; // SDL2 relative mouse mode (FPS camera)
_input.Cursor.IsConfined = true; // Confine to window bounds
When IsLocked is enabled, MonoGameInputProvider activates SDL2's SDL_SetRelativeMouseMode. This grabs the cursor and delivers raw hardware deltas via SDL_GetRelativeMouseState — no Mouse.SetPosition hack, no DPI jitter, no 1-frame lag. This is the same approach used by AAA games and engines like Unity/Unreal.
| Property | Description |
|---|---|
IsVisible |
Show/hide the OS cursor |
IsLocked |
Enable SDL2 relative mouse mode — cursor is grabbed and raw hardware deltas are delivered via SDL_GetRelativeMouseState, jitter-free |
IsConfined |
Clamp cursor to window bounds |
Custom Providers
You can replace MonoGameInputProvider with custom implementations — useful for testing, replay systems, or alternative input sources.
Implement one or more provider interfaces:
public class ReplayProvider : IKeyboardProvider, IMouseProvider
{
private readonly ReplayData _data;
private int _frameIndex;
public void Poll() => _frameIndex++;
public KeyboardSnapshot GetKeyboardState()
=> _data.KeyboardFrames[_frameIndex];
public MouseSnapshot GetMouseState()
=> _data.MouseFrames[_frameIndex];
}
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. A single object implementing multiple interfaces is polled only once per frame.
You can also register providers individually:
_input.UseKeyboard(myKeyboardProvider);
_input.UseMouse(myMouseProvider);
_input.UseTextInput(myTextProvider);
_input.UseGamepad(myGamepadProvider);
_input.UseTouch(myTouchProvider);
Provider Interfaces
| Interface | Methods |
|---|---|
IKeyboardProvider |
Poll(), GetKeyboardState() → KeyboardSnapshot |
IMouseProvider |
Poll(), GetMouseState() → MouseSnapshot |
ITextInputProvider |
Poll(), GetTextInputState() → TextInputSnapshot |
IGamepadProvider |
Poll(), GetGamepadState(PlayerIndex) → GamepadSnapshot |
ITouchProvider |
Poll(), GetTouchState() → TouchSnapshot |
Debugging
InputKit includes built-in debug tools accessible on InputManager:
// Overview of all registered layers, consumers, sinks, and middleware
string overview = _input.GetDebugOverview();
// Per-frame consumption report — what was consumed, by whom, at which layer
string report = _input.GetFrameReport();
// Programmatic access to consumption data
var consumptions = _input.GetConsumptions();
// Returns IReadOnlyDictionary<InputChannel, (int layer, string consumer)>
Example GetDebugOverview() output:
=== InputManager Overview ===
Layer 100:
[ACTIVE] PauseMenu
Layer 50:
[ACTIVE] Toolbar
Layer 0:
[ACTIVE] WorldInput
[SINK:DISABLED] PauseBlocker
Middleware (Legacy)
Deprecated. The
IInputMiddlewareinterface andUse()method still work but are superseded by Pre/Post-Dispatch Callbacks. New code should useOnPreDispatch/OnPostDispatchinstead.
The legacy middleware system uses an ASP.NET Core-style Execute(frame, next) chain. Each middleware must call next() to continue the pipeline — if it doesn't, the pipeline stops and no consumers run.
public class MyMiddleware : IInputMiddleware
{
public int Priority => 0; // Higher = executes first
public bool IsEnabled => true;
public void Execute(InputFrame frame, Action next)
{
// Pre-processing
frame.SetData(new MyData());
next(); // Continue to next middleware, then consumers
// Post-processing (runs after consumers)
}
}
_input.Use(new MyMiddleware());
_input.RemoveMiddleware(middleware);
Why callbacks are preferred over middleware:
- Simpler — No
next()chain to manage. All callbacks always run. - Explicit phases — Pre-dispatch and post-dispatch are clearly separated instead of wrapping
next(). - No accidental pipeline breaks — Forgetting
next()in middleware silently breaks the entire system. - Just a lambda — No interface to implement. Any
Action<InputFrame>works.
Middleware still executes in step 4 of the update cycle (before pre-dispatch), so it is compatible with the callback system.
API Reference
InputManager
| Member | Description |
|---|---|
InputManager() |
Create with no providers |
InputManager(object provider) |
Create and auto-register a provider |
AddProvider(object) |
Auto-register a provider for all interfaces it implements |
UseKeyboard(IKeyboardProvider) |
Register a keyboard provider |
UseMouse(IMouseProvider) |
Register a mouse provider |
UseTextInput(ITextInputProvider) |
Register a text input provider |
UseGamepad(IGamepadProvider) |
Register a gamepad provider |
UseTouch(ITouchProvider) |
Register a touch provider |
Register(IInputConsumer, int layer) |
Register a consumer at a specific layer |
Register(IInputConsumer) |
Register with auto-assigned layer |
Unregister(IInputConsumer) |
Remove a consumer from all layers |
RegisterSink(InputSink, int layer) |
Register an input sink |
UnregisterSink(InputSink) |
Remove an input sink |
OnPreDispatch(Action<InputFrame>, int priority) |
Add a pre-dispatch callback |
RemovePreDispatch(Action<InputFrame>) |
Remove a pre-dispatch callback |
OnPostDispatch(Action<InputFrame>, int priority) |
Add a post-dispatch callback |
RemovePostDispatch(Action<InputFrame>) |
Remove a post-dispatch callback |
Update(GameTime) |
Run the full input cycle for one frame |
Frame |
The current InputFrame (available after Update) |
Cursor |
The ICursorManager (if the mouse provider implements it) |
GetData<T>() |
Read data bag entries after Update() |
GetConsumptions() |
Get all consumptions for the current frame |
GetDebugOverview() |
Human-readable system overview |
GetFrameReport() |
Human-readable consumption report |
Use(IInputMiddleware) |
Legacy — add middleware |
RemoveMiddleware(IInputMiddleware) |
Legacy — remove middleware |
InputFrame
| Member | Type | Description |
|---|---|---|
Peek |
InputFrameView |
Non-consuming view respecting consumption |
Raw |
InputFrameView |
Non-consuming view ignoring consumption |
DeltaTime |
float |
Seconds since last frame |
FrameNumber |
uint |
Monotonic frame counter |
TotalTime |
double |
Total seconds since game start |
Modifiers |
InputModifiers |
Ctrl/Shift/Alt state |
MousePosition |
Vector2 |
Current mouse position (non-consuming) |
MouseDelta |
Vector2 |
Raw mouse delta (non-consuming) |
ScrollWheelDelta |
int |
Raw scroll delta (non-consuming) |
TextInput |
IReadOnlyList<char> |
Raw text characters (non-consuming) |
InputFrameView
Same query methods as InputFrame but never consumes. Used via frame.Peek and frame.Raw.
Additional members: DeltaTime, FrameNumber, TotalTime, Modifiers, MousePosition, GetPressedKeys(), IsConsumedAbove(InputChannel).
InputChannel
| Factory | Description |
|---|---|
Key(Keys) |
Keyboard key channel |
Mouse(MouseButton) |
Mouse button channel |
MouseDelta |
Mouse movement channel |
Scroll |
Scroll wheel channel |
TextInput |
Text input channel |
Gamepad(Buttons, PlayerIndex) |
Gamepad button channel |
Gamepad(GamepadAxis, PlayerIndex) |
Gamepad axis channel |
Touch |
Touch channel |
Custom(int id, int subId) |
User-defined channel |
Snapshots
| Type | Key Members |
|---|---|
KeyboardSnapshot |
WasKeyPressed(Keys), WasKeyReleased(Keys), IsKeyDown(Keys) |
MouseSnapshot |
WasButtonPressed(MouseButton), WasButtonReleased(MouseButton), IsButtonDown(MouseButton), Position, Delta, ScrollWheelDelta |
GamepadSnapshot |
WasButtonPressed(Buttons), WasButtonReleased(Buttons), IsButtonDown(Buttons), GetAxis(GamepadAxis), IsConnected |
TextInputSnapshot |
Characters, HasInput |
TouchSnapshot |
Touches, Count, HasTouches |
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.