A layered input management system for MonoGame. Provides consumption-based input handling with middleware pipelines, layer priorities, input sinks, and support for keyboard, mouse, gamepad, touch, and text input.
$ dotnet add package H073.InputKitA 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+
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:
ConcurrentQueue<char> and drained into a reusable list each frame.MonoGameInputProvider for test stubs, replay systems, or other frameworks.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);
}
}
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.
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);
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.
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 |
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 |
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
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:
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();
}
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 are the primary extensibility mechanism. They replace the old middleware system with a simpler, more flexible model.
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 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);
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
_input.RemovePreDispatch(tracker.Process);
_input.RemovePostDispatch(logger.Process);
| 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 is a per-frame, type-keyed Dictionary<Type, object> on InputFrame. It allows pre-dispatch code to write computed data that consumers read.
// One entry per type — type is the key
frame.SetData(new ChargeData { Progress = 0.75f });
frame.SetData(new ComboData { TriggeredCombo = "Konami" });
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 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.
InputKit includes two ready-to-use trackers that register as pre-dispatch callbacks and write their results to the data bag.
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.
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 |
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, debouncingAfter 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.
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.
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.
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();
}
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.
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 |
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 |
_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.
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 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().
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 |
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);
| Interface | Methods |
|---|---|
IKeyboardProvider | Poll(), GetKeyboardState() → KeyboardSnapshot |
IMouseProvider | Poll(), GetMouseState() → MouseSnapshot |
ITextInputProvider | Poll(), GetTextInputState() → TextInputSnapshot |
IGamepadProvider | Poll(), GetGamepadState(PlayerIndex) → GamepadSnapshot |
ITouchProvider | Poll(), GetTouchState() → TouchSnapshot |
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
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:
next() chain to manage. All callbacks always run.next().next() in middleware silently breaks the entire system.Action<InputFrame> works.Middleware still executes in step 4 of the update cycle (before pre-dispatch), so it is compatible with the callback system.
| 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 |
| 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) |
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).
| 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 |
| 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 |
MIT