InputMan.StrideConn 0.1.1

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