InputMan.Core 0.1.1

dotnet add package InputMan.Core --version 0.1.1
                    
NuGet\Install-Package InputMan.Core -Version 0.1.1
                    
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="InputMan.Core" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="InputMan.Core" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="InputMan.Core" />
                    
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 InputMan.Core --version 0.1.1
                    
#r "nuget: InputMan.Core, 0.1.1"
                    
#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 InputMan.Core@0.1.1
                    
#: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=InputMan.Core&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=InputMan.Core&version=0.1.1
                    
Install as a Cake Tool

InputMan

A powerful, flexible input management library for modern .NET game engines with first-class rebinding support.

NuGet License

InputMan provides a modern, engine-agnostic input system with action maps, priority-based consumption, and seamless runtime rebinding. Perfect for games that need professional input handling.

⚠️ Pre-Release Notice

This is v0.1.x - the initial public release. The API is functional and tested, but may evolve before v1.0 based on community feedback. Please report issues and suggestions on GitHub!


✨ Features

  • 🎮 Action Maps - Layer map based inputs with priority-based consumption (UI blocks gameplay, etc.)
  • 🔄 Runtime Rebinding - Full-featured RebindingManager with automatic profile saving
  • 🎯 Multiple Input Types - Buttons, axes, delta axes, and 2D axes
  • 🎹 Chord Bindings - Modifier keys (Shift+W for sprint, Ctrl+S for save, etc.)
  • ⚙️ Processors - Built-in deadzone, invert, and scale processors
  • 🎨 Engine-Agnostic Core - Use with any engine (Stride adapter included)
  • 💾 Pluggable Serialization - JSON by default, others (TOML, XML, binary, etc.) easy to add
  • 🎛️ Consumption Control - Higher-priority maps can block lower ones
  • 🔌 Type-safe IDs - Prefer static readonly IDs; strings are allowed for quick prototyping

📦 Installation

NuGet Packages

# Core library (engine-agnostic)
dotnet add package InputMan.Core

# Stride engine adapter
dotnet add package InputMan.StrideConn

Package Manager Console

Install-Package InputMan.Core
Install-Package InputMan.StrideConn

🚀 Quick Start (Stride Engine)

Step 1: Create Your Input Profile

Create MyGameProfile.cs to define your controls:

using InputMan.Core;
using InputMan.StrideConn;
using Stride.Input;
using static InputMan.Core.Bind;
using static InputMan.StrideConn.StrideKeys;

public static class MyGameProfile
{
    public static InputProfile Create()
    {
        var gameplay = new ActionMapDefinition
        {
            Id = new ActionMapId("Gameplay"),
            Priority = 10,
            Bindings =
            [
                // Jump on Space key
                Action(K(Keys.Space), new ActionId("Jump"), ButtonEdge.Pressed),
                
                // Jump on gamepad A button
                Action(PadBtn(0, GamePadButton.A), new ActionId("Jump"), ButtonEdge.Pressed),
            ]
        };
        
        return new InputProfile
        {
            Maps = new Dictionary<string, ActionMapDefinition>
            {
                ["Gameplay"] = gameplay
            }
        };
    }
}

Step 2: Install InputMan (30 seconds)

Create InstallInputMan.cs in your project:

using InputMan.Core;
using InputMan.StrideConn;
using Stride.Engine;

public class InstallInputMan : StartupScript
{
    public override void Start()
    {
        // Create profile storage
        var storage = StrideProfileStorage.CreateDefault(
            appName: "MyGame",
            defaultProfileFactory: MyGameProfile.Create);

        // Load profile (user > bundled > code default)
        var profile = storage.LoadProfile();

        // Install InputMan system
        var inputSystem = new StrideInputManSystem(
            Game.Services, 
            profile,
            new ActionMapId("Gameplay"));

        Game.GameSystems.Add(inputSystem);
    }
}

Drag this script onto your Game Manager entity in the scene.

Step 3: Read Input in Your Scripts

using InputMan.Core;
using Stride.Engine;

public class PlayerController : SyncScript
{
    private IInputMan _input;

    public override void Start()
    {
        _input = Game.Services.GetService<IInputMan>();
    }

    public override void Update()
    {
        // Check if jump was just pressed
        if (_input.WasPressed(new ActionId("Jump")))
        {
            Log.Info("Player jumped!");
        }
    }
}

That's it! You now have a working input system. 🎉


📚 Core Concepts

Actions vs Axes

Actions are discrete events (pressed, held, released):

var jumpAction = new ActionId("Jump");

// Was it just pressed this frame?
if (_input.WasPressed(jumpAction)) { }

// Is it currently held down?
if (_input.IsDown(jumpAction)) { }

// Was it just released this frame?
if (_input.WasReleased(jumpAction)) { }

Axes are continuous values (-1 to +1 for sticks, unbounded for mouse):

var moveXAxis = new AxisId("MoveX");

// Get current value
float horizontal = _input.GetAxis(moveXAxis);

Axis2 combines two axes into a Vector2:

var moveAxis = new Axis2Id("Move");

// Get both X and Y at once
Vector2 movement = _input.GetAxis2(moveAxis);

Action Maps

Action maps let you organize inputs into logical groups with priorities:

var profile = new InputProfile
{
    Maps = new Dictionary<string, ActionMapDefinition>
    {
        // UI map - highest priority (100)
        ["UI"] = new ActionMapDefinition
        {
            Id = new ActionMapId("UI"),
            Priority = 100,  // Higher number = evaluated first
            CanConsume = true,  // Can block lower-priority maps
            Bindings = [ /* UI bindings */ ]
        },
        
        // Gameplay map - lower priority (10)
        ["Gameplay"] = new ActionMapDefinition
        {
            Id = new ActionMapId("Gameplay"),
            Priority = 10,
            CanConsume = false,
            Bindings = [ /* Gameplay bindings */ ]
        }
    }
};

Activate maps at runtime:

// Show pause menu - UI blocks gameplay
_input.SetMaps(new ActionMapId("UI"));

// Resume - both active, UI has priority
_input.SetMaps(
    new ActionMapId("UI"),
    new ActionMapId("Gameplay"));

Bindings

Bindings connect physical controls to actions/axes:

using static InputMan.Core.Bind;
using static InputMan.StrideConn.StrideKeys;

var bindings = new List<Binding>
{
    // Button -> Action
    Action(K(Keys.Space), new ActionId("Jump"), ButtonEdge.Pressed),
    
    // Button -> Axis (WASD movement)
    ButtonAxis(K(Keys.W), new AxisId("MoveY"), +1.0f),
    ButtonAxis(K(Keys.S), new AxisId("MoveY"), -1.0f),
    
    // Analog Stick -> Axis
    Axis(PadLeftX(0), new AxisId("MoveX"), scale: 1.0f),
    
    // Mouse Delta -> Axis (camera look)
    DeltaAxis(MouseDeltaX, new AxisId("LookX"), scale: 1.0f),
};

Chord Bindings (Modifier Keys)

Create modifier-based bindings for advanced controls:

// Sprint with Shift+W
ActionChord(K(Keys.W), Sprint, ButtonEdge.Down, 
    name: "Sprint.Kb", 
    modifiers: K(Keys.LeftShift))

// Quick save with Ctrl+S
ActionChord(K(Keys.S), QuickSave, ButtonEdge.Pressed,
    name: "QuickSave.Kb",
    modifiers: K(Keys.LeftControl))

// Multiple modifiers: Ctrl+Shift+P
ActionChord(K(Keys.P), DebugPanel, ButtonEdge.Pressed,
    name: "Debug.Kb",
    modifiers: new[] { K(Keys.LeftControl), K(Keys.LeftShift) })

How chords work:

  • ALL modifier keys must be held simultaneously with the primary key
  • Release any modifier and the action deactivates (perfect for sprint)
  • Works with ButtonEdge.Down, Pressed, and Released

🎮 Complete Example: Third-Person Controller

Define Your Profile

using InputMan.Core;
using InputMan.StrideConn;
using static InputMan.Core.Bind;
using static InputMan.StrideConn.StrideKeys;

public static class MyGameProfile
{
    // Define IDs
    public static readonly ActionId Jump = new("Jump");
    public static readonly ActionId Sprint = new("Sprint");
    public static readonly AxisId MoveX = new("MoveX");
    public static readonly AxisId MoveY = new("MoveY");
    public static readonly Axis2Id Move = new("Move");
    
    public static InputProfile Create()
    {
        var deadzone = new DeadzoneProcessor(0.15f);
        
        var gameplay = new ActionMapDefinition
        {
            Id = new ActionMapId("Gameplay"),
            Priority = 10,
            Bindings =
            [
                // WASD Movement
                ButtonAxis(K(Keys.W), MoveY, +1f, name: "MoveFwd.Kb"),
                ButtonAxis(K(Keys.S), MoveY, -1f, name: "MoveBack.Kb"),
                ButtonAxis(K(Keys.A), MoveX, -1f, name: "MoveLeft.Kb"),
                ButtonAxis(K(Keys.D), MoveX, +1f, name: "MoveRight.Kb"),
                
                // Sprint (Shift+W)
                ActionChord(K(Keys.W), Sprint, ButtonEdge.Down, 
                    name: "Sprint.Kb", modifiers: K(Keys.LeftShift)),
                
                // Gamepad left stick (with deadzone)
                Axis(PadLeftX(0), MoveX, scale: 1f, processors: deadzone),
                Axis(PadLeftY(0), MoveY, scale: 1f, processors: deadzone),
                
                // Jump
                Action(K(Keys.Space), Jump, ButtonEdge.Pressed, name: "Jump.Kb"),
                Action(PadBtn(0, GamePadButton.A), Jump, ButtonEdge.Pressed),
            ]
        };
        
        return new InputProfile
        {
            Maps = new() { ["Gameplay"] = gameplay },
            Axis2 = new()
            {
                ["Move"] = new Axis2Definition 
                { 
                    Id = Move, 
                    X = MoveX, 
                    Y = MoveY 
                }
            }
        };
    }
}

Use in Your Controller

public class PlayerController : SyncScript
{
    private IInputMan _input;
    
    public float MoveSpeed = 5f;
    public float SprintSpeed = 10f;
    public float JumpForce = 10f;

    public override void Start()
    {
        _input = Game.Services.GetService<IInputMan>();
    }

    public override void Update()
    {
        float dt = (float)Game.UpdateTime.Elapsed.TotalSeconds;
        
        // Sprint when Shift+W is held
        var speed = _input.IsDown(MyGameProfile.Sprint) ? SprintSpeed : MoveSpeed;
        
        // Get movement input
        var moveInput = _input.GetAxis2(MyGameProfile.Move);
        var movement = new Vector3(moveInput.X, 0, moveInput.Y) * speed * dt;
        
        Entity.Transform.Position += movement;
        
        // Jump
        if (_input.WasPressed(MyGameProfile.Jump))
        {
            Jump();
        }
    }
}

🔄 Runtime Rebinding

InputMan includes a powerful RebindingManager that handles all rebinding logic for you. It's clean, reusable, and works with any UI system.

The easiest way to add rebinding to your game:

using InputMan.Core;
using InputMan.StrideConn;
using Stride.Engine;

public class SettingsMenu : SyncScript
{
    private IInputMan _inputMan;
    private RebindingManager _rebindManager;

    public override void Start()
    {
        _inputMan = Game.Services.GetService<IInputMan>();
        
        // Create storage for saving rebinds
        var storage = StrideProfileStorage.CreateDefault(
            appName: "MyGame",
            defaultProfileFactory: MyGameProfile.Create);
        
        // Create rebinding manager
        _rebindManager = new RebindingManager(_inputMan, storage);
        
        // Subscribe to status updates for UI feedback
        _rebindManager.OnStatusChanged += message => 
        {
            UpdateUI(message); // Show "Press a key..." etc.
        };
        
        _rebindManager.OnCompleted += success =>
        {
            if (success)
                ShowMessage("Binding saved!");
            else
                ShowMessage("Binding failed");
        };
    }

    public void OnRebindJumpButtonClicked()
    {
        // Build candidate buttons (what keys are allowed)
        var candidates = StrideCandidateButtons.KeyboardAndGamepad();
        
        // Start rebinding
        _rebindManager.StartRebind(
            bindingName: "Jump.Kb",
            map: new ActionMapId("Gameplay"),
            candidateButtons: candidates,
            forbiddenControls: new HashSet<ControlKey>
            {
                new(DeviceKind.Keyboard, 0, (int)Keys.Escape) // Reserve Escape
            },
            disallowConflicts: true);
    }
    
    public void OnCancelButtonClicked()
    {
        _rebindManager.CancelRebind();
    }
}

That's it! RebindingManager handles:

  • ✅ Session management
  • ✅ Progress tracking
  • ✅ Profile saving
  • ✅ Event notifications
  • ✅ Error handling

Candidate Buttons (Stride)

StrideCandidateButtons provides helpers for building button lists:

// Keyboard + Gamepad (most common for gameplay)
var candidates = StrideCandidateButtons.KeyboardAndGamepad();

// Keyboard + Mouse (for aim/look controls)
var candidates = StrideCandidateButtons.KeyboardAndMouse();

// All devices
var candidates = StrideCandidateButtons.AllDevices();

// Just keyboard
var candidates = StrideCandidateButtons.AllKeyboardKeys();

// Just mouse
var candidates = StrideCandidateButtons.AllMouseButtons();

// Smart: auto-detect from RebindRequest
var request = RebindPresets.GameplayButton(map, "Jump.Kb");
var candidates = StrideCandidateButtons.ForRequest(request);

⚙️ Processors

Transform input values with processors:

var binding = new Binding
{
    Trigger = new BindingTrigger
    {
        Control = PadLeftX(0),
        Type = TriggerType.Axis
    },
    Output = new AxisOutput(new AxisId("MoveX"), Scale: 1f),
    
    // Apply processors in order
    Processors = new List<IProcessor>
    {
        new DeadzoneProcessor(0.15f),  // Ignore small stick drift
        new ScaleProcessor(2.0f),      // Double sensitivity
        new InvertProcessor()          // Flip direction
    }
};

Built-in Processors:

  • DeadzoneProcessor(float deadzone) - Ignore input below threshold, remap above
  • ScaleProcessor(float scale) - Multiply input value
  • InvertProcessor() - Negate input value

Custom Processors:

public class ClampProcessor : IProcessor
{
    private readonly float _min, _max;
    
    public ClampProcessor(float min, float max)
    {
        _min = min;
        _max = max;
    }
    
    public float Process(float value)
    {
        return Math.Clamp(value, _min, _max);
    }
}

🎯 Advanced: Consumption

Control how maps interact with priority and consumption:

var uiMap = new ActionMapDefinition
{
    Id = new ActionMapId("UI"),
    Priority = 100,
    CanConsume = true,  // This map can consume inputs
    Bindings =
    [
        new Binding
        {
            Trigger = new BindingTrigger 
            { 
                Control = K(Keys.Escape), 
                Type = TriggerType.Button 
            },
            Output = new ActionOutput(new ActionId("CloseMenu")),
            Consume = ConsumeMode.All  // Consume both control AND action
        }
    ]
};

var gameplayMap = new ActionMapDefinition
{
    Id = new ActionMapId("Gameplay"),
    Priority = 10,
    CanConsume = false,  // Lower priority, can't consume
    Bindings = [ /* ... */ ]
};

Consume Modes:

  • ConsumeMode.None - Don't consume (multiple maps can read same input)
  • ConsumeMode.ControlOnly - Consume the physical control (Escape key blocked)
  • ConsumeMode.ActionOnly - Consume the action (CloseMenu fires only once)
  • ConsumeMode.All - Consume both control and action

Example: Pause Menu

// When pause menu opens
_input.SetMaps(
    new ActionMapId("UI"),         // Priority 100, will consume Escape
    new ActionMapId("Gameplay"));  // Priority 10, won't see Escape

// When menu closes
_input.SetMaps(new ActionMapId("Gameplay"));  // Full control restored

🔧 Troubleshooting

"IInputMan not found"

Make sure InstallInputMan runs before other scripts:

// In InstallInputMan.cs
public class InstallInputMan : StartupScript  // ← StartupScript runs early
{
    // ...
}

Input not responding

Check that your map is activated:

public override void Start()
{
    _input = Game.Services.GetService<IInputMan>();
    
    // Make sure to activate your map!
    _input.SetMaps(new ActionMapId("Gameplay"));
}

Rebinding doesn't work

Use RebindingManager and provide candidate buttons:

var candidates = StrideCandidateButtons.KeyboardAndGamepad();
_rebindManager.StartRebind("Jump.Kb", map, candidates);

Profile changes not saving

Make sure you're using RebindingManager with IProfileStorage:

// RebindingManager auto-saves on successful rebind
var storage = StrideProfileStorage.CreateDefault("MyGame", MyGameProfile.Create);
var rebindManager = new RebindingManager(_inputMan, storage);

📖 API Reference

IInputMan Interface

// State queries
bool IsDown(ActionId action);
bool WasPressed(ActionId action);
bool WasReleased(ActionId action);
float GetAxis(AxisId axis);
Vector2 GetAxis2(Axis2Id axis2);

// Map management
void PushMap(ActionMapId map, int? priorityOverride = null);
void PopMap(ActionMapId map);
void SetMaps(params ActionMapId[] maps);

// Rebinding
IRebindSession StartRebind(RebindRequest request);

// Profile management
InputProfile ExportProfile();
void ImportProfile(InputProfile profile);

// Events
event Action<ActionEvent> OnAction;
event Action<AxisEvent> OnAxis;

// Frame info
long FrameIndex { get; }
float DeltaTimeSeconds { get; }

RebindingManager (Core)

// Constructor
RebindingManager(IInputMan inputMan, IProfileStorage storage);

// Properties
bool IsRebinding { get; }
string StatusMessage { get; }

// Methods
void StartRebind(string bindingName, ActionMapId map, 
    IReadOnlyList<ControlKey> candidateButtons,
    IReadOnlySet<ControlKey>? forbiddenControls = null,
    bool disallowConflicts = true);
void CancelRebind();

// Events
event Action<string> OnStatusChanged;
event Action<bool> OnCompleted;

StrideCandidateButtons (StrideConn)

List<ControlKey> AllKeyboardKeys();
List<ControlKey> AllMouseButtons();
List<ControlKey> KeyboardAndGamepad();
List<ControlKey> KeyboardAndMouse();
List<ControlKey> AllDevices();
List<ControlKey> ForRequest(RebindRequest request);

Bind Helpers

// Button -> Action
Binding Action(ControlKey key, ActionId action, ButtonEdge edge = Pressed)

// Button -> Action with Modifiers (Chord)
Binding ActionChord(ControlKey key, ActionId action, ButtonEdge edge = Down,
    ConsumeMode consume = None, string? name = null, params ControlKey[] modifiers)

// Button -> Axis (WASD-style)
Binding ButtonAxis(ControlKey key, AxisId axis, float scale)

// Analog -> Axis (sticks, triggers)
Binding Axis(ControlKey key, AxisId axis, float scale = 1f, 
    float threshold = 0f, ConsumeMode consume = None, string? name = null,
    params IProcessor[] processors)

// Delta -> Axis (mouse movement)
Binding DeltaAxis(ControlKey key, AxisId axis, float scale = 1f)

🎓 Learning Resources

Sample Projects

Check out the ThirdPersonPlatformer demo in the repository for a complete working example with:

  • ✅ WASD + Gamepad movement
  • ✅ Mouse + Stick camera control
  • ✅ Sprint with Shift+W (chord binding)
  • ✅ Runtime rebinding with RebindingManager
  • ✅ Pause menu with map switching
  • ✅ Profile saving/loading with StrideProfileStorage

📄 License

MIT License - see LICENSE file for details


🤝 Contributing

This is v0.1.0 - feedback and contributions are welcome! Please:

  • Report bugs and issues on GitHub
  • Suggest features and improvements
  • Share your use cases and experiences

Made with ❤️ for game developers

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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.
  • net8.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on InputMan.Core:

Package Downloads
InputMan.StrideConn

Stride connector for InputMan. Provides Stride input snapshot building, control codes, profile storage helpers, and a Stride game system runner so InputMan.Core can drive gameplay + UI input cleanly inside Stride projects.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 75 2/19/2026
0.1.0 84 2/19/2026