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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="H073.InputKit" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="H073.InputKit" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="H073.InputKit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add H073.InputKit --version 1.1.0
                    
#r "nuget: H073.InputKit, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package H073.InputKit@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=H073.InputKit&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=H073.InputKit&version=1.1.0
                    
Install as a Cake Tool

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

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:

  1. Layer-based consumption — Higher layers process input first and can consume it, blocking lower layers.
  2. 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.
  3. No per-frame allocation — Snapshots are readonly structs holding only current + previous hardware state.
  4. Thread-safe text input — OS text input events are collected via ConcurrentQueue<char> and drained into a reusable list each frame.
  5. Framework-agnostic core — Provider interfaces abstract hardware access. Swap MonoGameInputProvider for 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, cooldowns
  • FrameNumber — replay indexing, deterministic simulation, networking
  • TotalTime — 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 IInputMiddleware interface and Use() method still work but are superseded by Pre/Post-Dispatch Callbacks. New code should use OnPreDispatch / OnPostDispatch instead.

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:

  1. Simpler — No next() chain to manage. All callbacks always run.
  2. Explicit phases — Pre-dispatch and post-dispatch are clearly separated instead of wrapping next().
  3. No accidental pipeline breaks — Forgetting next() in middleware silently breaks the entire system.
  4. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 99 2/20/2026
1.0.0 94 2/7/2026