H073.InputKit 1.0.0

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package H073.InputKit --version 1.0.0
                    
NuGet\Install-Package H073.InputKit -Version 1.0.0
                    
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.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="H073.InputKit" Version="1.0.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.0.0
                    
#r "nuget: H073.InputKit, 1.0.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.0.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.0.0
                    
Install as a Cake Addin
#tool nuget:?package=H073.InputKit&version=1.0.0
                    
Install as a Cake Tool

Note: This README was generated with the help of AI. The library code itself is entirely human-written and AI-free.

H073.InputKit

A layered, consumption-based input management system for MonoGame. InputKit gives you a clean, structured way to handle keyboard, mouse, gamepad, touch, and text input across multiple layers with automatic input consumption, middleware pipelines, and input sinks.

Features

  • Consumption-based input — Higher-priority layers consume input, preventing lower layers from seeing it
  • Middleware pipeline — ASP.NET Core-style Execute(frame, next) middleware for preprocessing input (input buffering, combo detection, dead zones, etc.)
  • Input sinks — Block all input propagation below a certain layer (useful for modals, pause menus)
  • Multi-device support — Keyboard, mouse, gamepad (4 players), touch, and text input
  • Data bag — Middleware can attach arbitrary typed data to frames for consumers to read
  • Peek & Raw views — Inspect input state without consuming it, or bypass consumption entirely
  • Cursor management — Lock, confine, and hide the OS cursor
  • Provider abstraction — Swap out MonoGame input for custom/test providers

Installation

dotnet add package H073.InputKit

Quick Start

using InputKit.Core;
using InputKit.Provider;

public class MyGame : Game
{
    private InputManager _input;

    protected override void Initialize()
    {
        _input = new InputManager(new MonoGameInputProvider(Window, GraphicsDevice));

        // Register a consumer at layer 0
        _input.Register(new PlayerController(), layer: 0);

        base.Initialize();
    }

    protected override void Update(GameTime gameTime)
    {
        _input.Update(gameTime);
        base.Update(gameTime);
    }
}

A consumer implements IInputConsumer:

using InputKit.Core;
using Microsoft.Xna.Framework.Input;

public class PlayerController : IInputConsumer
{
    public bool IsInputEnabled => true;

    public void ProcessInput(InputFrame frame)
    {
        if (frame.WasKeyPressed(Keys.Space))
            Jump();

        if (frame.IsKeyDown(Keys.A))
            MoveLeft();

        if (frame.WasMouseButtonPressed(MouseButton.Left))
            Attack(frame.MousePosition);
    }
}

Core Concepts

Layers & Consumption

Consumers are registered at numeric layers. Higher layers process input first. When a consumer reads input through a consuming method (e.g. WasKeyPressed), that input is marked as consumed at that layer. Lower layers will see false for the same input.

// Layer 100 = UI (highest priority)
// Layer 50  = HUD
// Layer 0   = Gameplay (lowest priority)

_input.Register(uiPanel,          layer: 100);
_input.Register(hudController,    layer: 50);
_input.Register(playerController, layer: 0);

If the UI panel consumes Keys.Escape at layer 100, neither the HUD nor the player controller will see it.

Consuming vs. Non-Consuming Access

InputFrame provides three ways to read input:

public void ProcessInput(InputFrame frame)
{
    // 1. Consuming (default) — marks input as consumed at your layer
    bool pressed = frame.WasKeyPressed(Keys.Space);

    // 2. Peek — sees all input INCLUDING what higher layers consumed
    //    Does NOT consume anything itself.
    bool peeked = frame.Peek.WasKeyPressed(Keys.Space);

    // 3. Raw — ignores consumption entirely, sees true hardware state
    //    Does NOT consume anything itself.
    bool raw = frame.Raw.WasKeyPressed(Keys.Space);
}

When to use each:

  • Consuming (default): Normal gameplay — "I want to use this input and prevent others from seeing it"
  • Peek: Debugging, analytics, or visual feedback — "I want to know what happened but not interfere"
  • Raw: Middleware or global hotkeys — "I need the true hardware state regardless of consumption"

Input Sinks

An InputSink blocks all input propagation below its layer. Useful for modal dialogs, pause menus, or loading screens.

var pauseSink = new InputSink("PauseMenu");
_input.RegisterSink(pauseSink, layer: 90);

// When the pause menu opens:
pauseSink.IsEnabled = true;
// Now all consumers below layer 90 receive no input at all.

// When the pause menu closes:
pauseSink.IsEnabled = false;
// Input flows normally again.

Bulk Consumption

Sometimes you want to consume entire categories of input at once:

public void ProcessInput(InputFrame frame)
{
    // Consume ALL keyboard input at this layer
    frame.ConsumeAllKeyboard();

    // Consume all keyboard input EXCEPT certain keys
    frame.ConsumeAllKeyboard(except: Keys.Escape, Keys.F1);

    // Consume keyboard input matching a predicate
    frame.ConsumeKeyboardWhere(key => key >= Keys.A && key <= Keys.Z);

    // Consume all mouse input (buttons, delta, scroll)
    frame.ConsumeAllMouse();

    // Consume text input characters
    frame.ConsumeTextInput();

    // Consume touch input
    frame.ConsumeTouch();

    // Nuclear option: consume everything
    frame.ConsumeAll();
}

Modifier Keys

Check modifier state without consuming:

public void ProcessInput(InputFrame frame)
{
    if (frame.Modifiers.Ctrl && frame.WasKeyPressed(Keys.S))
        Save();

    if (frame.Modifiers.Shift && frame.WasKeyPressed(Keys.Z))
        Redo();

    // Distinguish left/right modifiers
    if (frame.Modifiers.LeftAlt)
        DoSomething();
}

Middleware

Middleware processes the InputFrame before it reaches any consumer. This is the right place for input buffering, combo detection, dead zone filtering, or any preprocessing.

Writing Middleware

using InputKit.Core;

public class MyMiddleware : IInputMiddleware
{
    public int Priority => 0;        // Higher = executes first
    public bool IsEnabled => true;

    public void Execute(InputFrame frame, Action next)
    {
        // Pre-processing: inspect or modify frame state

        next();  // Pass to the next middleware (or consumers)

        // Post-processing: react to what consumers did
    }
}

Register it:

_input.Use(new MyMiddleware());

Example: Input Buffer

An input buffer remembers recent key presses so that actions can be triggered slightly after the key was pressed (common in platformers for jump buffering).

public class InputBuffer : IInputMiddleware
{
    public int Priority => 100;
    public bool IsEnabled => true;

    private readonly Dictionary<Keys, float> _buffer = new();
    private readonly float _bufferDuration;
    private float _deltaTime;

    public InputBuffer(float bufferDurationSeconds = 0.15f)
    {
        _bufferDuration = bufferDurationSeconds;
    }

    public void Execute(InputFrame frame, Action next)
    {
        _deltaTime = /* get from GameTime or frame data */;

        // Record fresh presses
        foreach (Keys key in Enum.GetValues<Keys>())
        {
            if (frame.Raw.WasKeyPressed(key))
                _buffer[key] = _bufferDuration;
        }

        // Tick down timers
        var expired = new List<Keys>();
        foreach (var (key, time) in _buffer)
        {
            _buffer[key] = time - _deltaTime;
            if (_buffer[key] <= 0)
                expired.Add(key);
        }
        foreach (var key in expired)
            _buffer.Remove(key);

        // Attach data for consumers to read
        frame.SetData(new InputBufferData(this));
        next();
    }

    public bool IsBuffered(Keys key)
        => _buffer.ContainsKey(key) && _buffer[key] > 0;

    public void ClearBuffer(Keys key)
        => _buffer.Remove(key);
}

public class InputBufferData
{
    private readonly InputBuffer _buffer;
    internal InputBufferData(InputBuffer buffer) => _buffer = buffer;

    public bool IsBuffered(Keys key) => _buffer.IsBuffered(key);
    public void Clear(Keys key) => _buffer.ClearBuffer(key);
}

Consumers read the buffer via the data bag:

public void ProcessInput(InputFrame frame)
{
    var buffer = frame.GetData<InputBufferData>();

    // Jump if Space was pressed recently (even a few frames ago)
    if (buffer?.IsBuffered(Keys.Space) == true && _isGrounded)
    {
        Jump();
        buffer.Clear(Keys.Space);  // Don't re-trigger
    }
}

Example: Combo Detection

Detect input sequences like fighting game combos or cheat codes.

public class ComboDetector : IInputMiddleware
{
    public int Priority => 90;
    public bool IsEnabled => true;

    private readonly List<Keys> _history = new();
    private float _comboTimer;
    private readonly float _comboWindow;

    public ComboDetector(float comboWindowSeconds = 0.5f)
    {
        _comboWindow = comboWindowSeconds;
    }

    public void Execute(InputFrame frame, Action next)
    {
        // Track pressed keys
        foreach (Keys key in Enum.GetValues<Keys>())
        {
            if (frame.Raw.WasKeyPressed(key))
            {
                _history.Add(key);
                _comboTimer = _comboWindow;
            }
        }

        // Expire old input
        _comboTimer -= 0.016f; // or get real delta from frame data
        if (_comboTimer <= 0)
            _history.Clear();

        // Check for combos and publish results
        var combos = new ComboData();

        if (MatchesSequence(Keys.Up, Keys.Up, Keys.Down, Keys.Down))
        {
            combos.TriggeredCombos.Add("KonamiStart");
            _history.Clear();
        }

        if (MatchesSequence(Keys.A, Keys.B, Keys.A))
        {
            combos.TriggeredCombos.Add("SpecialAttack");
            _history.Clear();
        }

        frame.SetData(combos);
        next();
    }

    private bool MatchesSequence(params Keys[] sequence)
    {
        if (_history.Count < sequence.Length) return false;
        var recent = _history.Skip(_history.Count - sequence.Length).ToArray();
        return recent.SequenceEqual(sequence);
    }
}

public class ComboData
{
    public List<string> TriggeredCombos { get; } = new();
}

Consumer side:

public void ProcessInput(InputFrame frame)
{
    var combos = frame.GetData<ComboData>();
    if (combos != null)
    {
        foreach (var combo in combos.TriggeredCombos)
        {
            switch (combo)
            {
                case "SpecialAttack": DoSpecialAttack(); break;
                case "KonamiStart":  UnlockSecret();    break;
            }
        }
    }
}

Example: Gamepad Dead Zone

Filter out stick drift by applying a dead zone in middleware:

public class DeadZoneMiddleware : IInputMiddleware
{
    public int Priority => 50;
    public bool IsEnabled => true;

    private readonly float _threshold;

    public DeadZoneMiddleware(float threshold = 0.15f)
    {
        _threshold = threshold;
    }

    public void Execute(InputFrame frame, Action next)
    {
        // Read raw axis values and apply dead zone
        float lx = frame.Raw.GetGamepadAxis(GamepadAxis.LeftStickX, PlayerIndex.One);
        float ly = frame.Raw.GetGamepadAxis(GamepadAxis.LeftStickY, PlayerIndex.One);

        var filtered = new DeadZoneData
        {
            LeftStick = ApplyDeadZone(lx, ly)
        };

        frame.SetData(filtered);
        next();
    }

    private Vector2 ApplyDeadZone(float x, float y)
    {
        var v = new Vector2(x, y);
        float magnitude = v.Length();
        if (magnitude < _threshold) return Vector2.Zero;

        // Rescale so edge of dead zone maps to 0
        float scaled = (magnitude - _threshold) / (1f - _threshold);
        return v * (scaled / magnitude);
    }
}

public class DeadZoneData
{
    public Vector2 LeftStick { get; init; }
}

Middleware Priority

Middleware executes in descending priority order (highest first). This lets you control the order of preprocessing:

_input.Use(new InputBuffer       { /* Priority = 100 */ }); // First: record inputs
_input.Use(new ComboDetector     { /* Priority = 90  */ }); // Second: detect combos
_input.Use(new DeadZoneMiddleware { /* Priority = 50  */ }); // Third: filter axes

Mouse Input

public void ProcessInput(InputFrame frame)
{
    // Button state (consuming)
    if (frame.WasMouseButtonPressed(MouseButton.Left))
        OnClick(frame.MousePosition);

    if (frame.IsMouseButtonDown(MouseButton.Right))
        DrawSelectionBox(frame.MousePosition);

    if (frame.WasMouseButtonReleased(MouseButton.Middle))
        StopPanning();

    // Movement delta (consuming)
    Vector2 delta = frame.GetMouseDelta();
    RotateCamera(delta);

    // Scroll wheel (consuming)
    int scroll = frame.GetScrollDelta();
    ZoomCamera(scroll);

    // Position is always available (non-consuming, read-only)
    Vector2 pos = frame.MousePosition;
}

Cursor Management

If your provider implements ICursorManager (the built-in MonoGameInputProvider does):

// Access through InputManager
_input.Cursor.IsVisible = false;   // Hide OS cursor
_input.Cursor.IsLocked = true;     // Lock to window center (FPS-style)
_input.Cursor.IsConfined = true;   // Confine to window bounds

Gamepad Input

InputKit supports up to 4 gamepads via PlayerIndex:

public void ProcessInput(InputFrame frame)
{
    if (!frame.IsGamepadConnected(PlayerIndex.One))
        return;

    // Buttons
    if (frame.WasGamepadButtonPressed(Buttons.A, PlayerIndex.One))
        Jump();

    if (frame.IsGamepadButtonDown(Buttons.RightTrigger, PlayerIndex.One))
        Accelerate();

    // Axes
    float stickX = frame.GetGamepadAxis(GamepadAxis.LeftStickX, PlayerIndex.One);
    float stickY = frame.GetGamepadAxis(GamepadAxis.LeftStickY, PlayerIndex.One);
    Move(new Vector2(stickX, stickY));

    float triggerR = frame.GetGamepadAxis(GamepadAxis.RightTrigger, PlayerIndex.One);
}

Local Multiplayer

_input.Register(new PlayerController(PlayerIndex.One),   layer: 0);
_input.Register(new PlayerController(PlayerIndex.Two),   layer: 0);
_input.Register(new PlayerController(PlayerIndex.Three), layer: 0);
_input.Register(new PlayerController(PlayerIndex.Four),  layer: 0);

Each controller queries its own PlayerIndex, so gamepad input is naturally isolated per player.

Touch Input

public void ProcessInput(InputFrame frame)
{
    var touches = frame.GetTouches();

    foreach (var touch in touches)
    {
        switch (touch.State)
        {
            case TouchLocationState.Pressed:
                OnTouchDown(touch.Position);
                break;
            case TouchLocationState.Moved:
                OnTouchMove(touch.Position);
                break;
            case TouchLocationState.Released:
                OnTouchUp(touch.Position);
                break;
        }
    }
}

Text Input

Text input is separate from keyboard key states. It captures the actual characters the user types (respecting OS keyboard layout, IME, etc.):

public void ProcessInput(InputFrame frame)
{
    var chars = frame.GetTextInput();

    foreach (char c in chars)
    {
        if (c == '\b')
            DeleteLastCharacter();
        else
            AppendCharacter(c);
    }
}

Custom Input Channels

InputKit's InputChannel supports a Custom kind for application-specific input concepts. This is useful when you want to define virtual inputs that don't map directly to hardware.

// Define custom channel IDs
static class VirtualInput
{
    // Custom(kind, id) — both are ints you define
    public static readonly InputChannel Jump       = InputChannel.Custom(1, 0);
    public static readonly InputChannel Attack     = InputChannel.Custom(1, 1);
    public static readonly InputChannel Interact   = InputChannel.Custom(1, 2);
    public static readonly InputChannel OpenMenu   = InputChannel.Custom(2, 0);
}

You can consume and query custom channels through the generic consumption API:

public void ProcessInput(InputFrame frame)
{
    // Check if a custom channel was already consumed by a higher layer
    if (!frame.IsConsumedAbove(VirtualInput.Jump))
    {
        frame.Consume(VirtualInput.Jump);
        Jump();
    }
}

A middleware can map hardware input to custom channels and publish the mapping via the data bag, creating a full input remapping system.

Custom Providers

You can swap out the MonoGame provider entirely — useful for testing, replays, or alternative input sources.

Implement one or more provider interfaces:

public class ReplayProvider : IKeyboardProvider, IMouseProvider
{
    private readonly Queue<KeyboardSnapshot> _keyboardFrames;
    private readonly Queue<MouseSnapshot> _mouseFrames;

    public void Poll() { /* advance to next frame */ }

    public KeyboardSnapshot GetKeyboardState()
        => _keyboardFrames.Dequeue();

    public MouseSnapshot GetMouseState()
        => _mouseFrames.Dequeue();
}

Register it:

var replay = new ReplayProvider(recordedData);
var input = new InputManager(replay);
// or:
var input = new InputManager();
input.AddProvider(replay);

AddProvider(object) auto-detects which interfaces the provider implements and registers each one.

You can also register providers individually:

_input.UseKeyboard(myKeyboardProvider);
_input.UseMouse(myMouseProvider);
_input.UseGamepad(myGamepadProvider);

Debugging

InputKit includes built-in debug tools:

// Overview of registered consumers, middleware, and sinks
string overview = _input.GetDebugOverview();

// Per-frame consumption report (what was consumed, by whom, at which layer)
string report = _input.GetFrameReport();

Full Example: Game with UI Layer

public class MyGame : Game
{
    private InputManager _input;
    private InputSink _modalSink;

    protected override void Initialize()
    {
        var provider = new MonoGameInputProvider(Window, GraphicsDevice);
        _input = new InputManager(provider);

        // Middleware
        _input.Use(new InputBuffer(0.12f));
        _input.Use(new DeadZoneMiddleware(0.2f));

        // Consumers (high layer = high priority)
        _input.Register(new ModalDialog(),    layer: 200);
        _input.Register(new UISystem(),       layer: 100);
        _input.Register(new PlayerControl(),  layer: 0);

        // Sink for blocking input during modals
        _modalSink = new InputSink("ModalBlocker");
        _input.RegisterSink(_modalSink, layer: 150);

        base.Initialize();
    }

    public void ShowModal()
    {
        _modalSink.IsEnabled = true;
        // Now only layer 200 (ModalDialog) receives input.
        // Layer 100 (UISystem) and 0 (PlayerControl) are blocked.
    }

    public void HideModal()
    {
        _modalSink.IsEnabled = false;
        // All layers receive input again.
    }

    protected override void Update(GameTime gameTime)
    {
        _input.Update(gameTime);
        base.Update(gameTime);
    }
}

API Reference Summary

Type Description
InputManager Main orchestrator — registers providers, middleware, consumers
InputFrame Per-frame input state with consuming methods
InputFrameView Non-consuming view (Peek or Raw)
IInputConsumer Interface for layer-based input consumers
IInputMiddleware ASP.NET Core-style middleware interface
InputSink Blocks input propagation below its layer
InputChannel Generic channel identifier (Key, Mouse, Gamepad, Touch, Custom)
InputModifiers Ctrl/Shift/Alt modifier state
ICursorManager Cursor visibility, lock, and confinement
MonoGameInputProvider Built-in provider for MonoGame

License

MIT

Product 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 100 2/20/2026
1.0.0 94 2/7/2026