Input Handling in MonoGame

MonoGame provides a straightforward polling input API: each frame you call a static method to capture the current hardware state, then query that snapshot. This article covers all four input sources — keyboard, mouse, gamepad, and touch — and shows how to build a reusable InputManager that tracks both the current and previous frame states for reliable “just pressed” detection.

💡 MonoGame’s input API is polling-based, not event-based. Always call GetState() once per frame at the top of Update() and store the result, rather than calling it multiple times.

Keyboard Input

Keyboard.GetState() returns a KeyboardState struct that is a snapshot of every key on the keyboard at that instant. The two most useful methods are:

  • IsKeyDown(Keys key) — returns true while the key is held
  • IsKeyUp(Keys key) — returns true while the key is not held
// In Update()
KeyboardState current = Keyboard.GetState();

// Held — true every frame the key is down
if (current.IsKeyDown(Keys.Space))
    player.Jump();

// Held — typical WASD movement
float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (current.IsKeyDown(Keys.A)) player.Position.X -= 200f * delta;
if (current.IsKeyDown(Keys.D)) player.Position.X += 200f * delta;
if (current.IsKeyDown(Keys.W)) player.Position.Y -= 200f * delta;
if (current.IsKeyDown(Keys.S)) player.Position.Y += 200f * delta;

// Getting all currently pressed keys
Keys[] pressedKeys = current.GetPressedKeys();
foreach (Keys k in pressedKeys)
    Console.WriteLine($"Pressed: {k}");

Detecting a key that was just pressed (i.e. down this frame but up last frame) requires comparing two frames. Store the previous state:

private KeyboardState _previousKeyboard;
private KeyboardState _currentKeyboard;

protected override void Update(GameTime gameTime)
{
    _previousKeyboard = _currentKeyboard;
    _currentKeyboard  = Keyboard.GetState();

    // Just pressed: down NOW, up LAST frame
    if (_currentKeyboard.IsKeyDown(Keys.Enter)
        && _previousKeyboard.IsKeyUp(Keys.Enter))
    {
        ConfirmSelection();
    }

    // Just released: up NOW, down LAST frame
    if (_currentKeyboard.IsKeyUp(Keys.Space)
        && _previousKeyboard.IsKeyDown(Keys.Space))
    {
        EndCharge();
    }

    base.Update(gameTime);
}

⚠️ Do not compare KeyboardState structs with == for just-pressed detection — use IsKeyDown / IsKeyUp on both the current and previous states as shown above.

Mouse Input

Mouse.GetState() returns a MouseState struct containing the cursor position, all button states, and the scroll wheel delta.

private MouseState _previousMouse;
private MouseState _currentMouse;

protected override void Update(GameTime gameTime)
{
    _previousMouse = _currentMouse;
    _currentMouse  = Mouse.GetState();

    // Cursor position in window coordinates
    int mouseX = _currentMouse.X;
    int mouseY = _currentMouse.Y;
    Vector2 mousePos = new Vector2(mouseX, mouseY);

    // Left button held
    if (_currentMouse.LeftButton == ButtonState.Pressed)
        Shoot(mousePos);

    // Right button just pressed
    bool rightJustPressed =
        _currentMouse.RightButton  == ButtonState.Pressed &&
        _previousMouse.RightButton == ButtonState.Released;
    if (rightJustPressed)
        OpenContextMenu(mousePos);

    // Scroll wheel — value is cumulative; take the delta between frames
    int scrollDelta = _currentMouse.ScrollWheelValue
                    - _previousMouse.ScrollWheelValue;
    if (scrollDelta > 0) ZoomIn();
    if (scrollDelta < 0) ZoomOut();

    base.Update(gameTime);
}

You can also reposition the cursor programmatically:

// Warp cursor to centre of window (useful for FPS-style camera look)
int centreX = GraphicsDevice.Viewport.Width  / 2;
int centreY = GraphicsDevice.Viewport.Height / 2;
Mouse.SetPosition(centreX, centreY);

// Hide cursor for full-screen games
IsMouseVisible = false;

💡 Use the scroll wheel delta (current minus previous) rather than the raw cumulative value, which grows unboundedly during a session.

Gamepad / Controller Input

MonoGame supports up to four gamepads via the XInput API on Windows (and the equivalent on other platforms). Call GamePad.GetState(PlayerIndex) to retrieve a GamePadState for a specific player slot.

private GamePadState _previousPad;
private GamePadState _currentPad;

protected override void Update(GameTime gameTime)
{
    _previousPad = _currentPad;
    _currentPad  = GamePad.GetState(PlayerIndex.One);

    // Only process input if a controller is connected
    if (!_currentPad.IsConnected) return;

    float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;

    // Left thumbstick — Vector2 with components in [-1, 1]
    Vector2 leftStick = _currentPad.ThumbSticks.Left;

    // Apply a small dead zone to ignore stick drift
    const float DeadZone = 0.15f;
    if (leftStick.Length() > DeadZone)
    {
        // Y axis is inverted relative to screen space
        player.Position.X += leftStick.X *  200f * delta;
        player.Position.Y += leftStick.Y * -200f * delta;
    }

    // Right thumbstick for camera
    Vector2 rightStick = _currentPad.ThumbSticks.Right;
    if (rightStick.Length() > DeadZone)
        camera.Rotate(rightStick.X * 90f * delta);

    // Triggers — float in [0, 1]
    float leftTrigger  = _currentPad.Triggers.Left;
    float rightTrigger = _currentPad.Triggers.Right;
    if (rightTrigger > 0.5f) Shoot();

    // Face buttons
    if (_currentPad.Buttons.A == ButtonState.Pressed
        && _previousPad.Buttons.A == ButtonState.Released)
    {
        Jump(); // just pressed
    }

    // Shoulder buttons
    if (_currentPad.Buttons.LeftShoulder == ButtonState.Pressed)
        Dash(Direction.Left);

    base.Update(gameTime);
}

To make a controller rumble (where supported by the platform):

// Set vibration — leftMotor and rightMotor are floats in [0, 1]
// Left motor = low-frequency rumble; right motor = high-frequency
GamePad.SetVibration(PlayerIndex.One, leftMotor: 0.8f, rightMotor: 0.3f);

// Stop vibration after a short duration using a timer
private float _rumbleTimer = 0f;

protected override void Update(GameTime gameTime)
{
    if (_rumbleTimer > 0f)
    {
        _rumbleTimer -= (float)gameTime.ElapsedGameTime.TotalSeconds;
        if (_rumbleTimer <= 0f)
            GamePad.SetVibration(PlayerIndex.One, 0f, 0f); // stop
    }
}

public void TriggerRumble(float duration = 0.25f)
{
    GamePad.SetVibration(PlayerIndex.One, 0.7f, 0.4f);
    _rumbleTimer = duration;
}

💡 Always stop vibration explicitly when done — the motors stay on until you set them to zero or the game closes.

Building a Reusable InputManager

Rather than scattering state storage across every class, centralise all input in a single InputManager that is updated once per frame and queried everywhere:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace MySpriteGame;

/// <summary>
/// Centralised input manager. Call Update() once per frame, then query
/// IsKeyDown, IsKeyJustPressed, etc. from anywhere.
/// </summary>
public class InputManager
{
    // Keyboard
    private KeyboardState _kPrev;
    private KeyboardState _kCurr;

    // Mouse
    private MouseState _mPrev;
    private MouseState _mCurr;

    // Gamepad (player one)
    private GamePadState _gPrev;
    private GamePadState _gCurr;

    public Vector2 MousePosition => new Vector2(_mCurr.X, _mCurr.Y);
    public Vector2 MouseDelta    => new Vector2(_mCurr.X - _mPrev.X, _mCurr.Y - _mPrev.Y);
    public int     ScrollDelta   => _mCurr.ScrollWheelValue - _mPrev.ScrollWheelValue;
    public bool    GamePadConnected => _gCurr.IsConnected;

    // -----------------------------------------------------------------------
    // Update — call exactly once per frame at the start of Game.Update()
    // -----------------------------------------------------------------------
    public void Update()
    {
        _kPrev = _kCurr;
        _kCurr = Keyboard.GetState();

        _mPrev = _mCurr;
        _mCurr = Mouse.GetState();

        _gPrev = _gCurr;
        _gCurr = GamePad.GetState(PlayerIndex.One);
    }

    // -----------------------------------------------------------------------
    // Keyboard helpers
    // -----------------------------------------------------------------------
    public bool IsKeyDown(Keys key)        => _kCurr.IsKeyDown(key);
    public bool IsKeyUp(Keys key)          => _kCurr.IsKeyUp(key);
    public bool IsKeyJustPressed(Keys key) => _kCurr.IsKeyDown(key) && _kPrev.IsKeyUp(key);
    public bool IsKeyJustReleased(Keys key)=> _kCurr.IsKeyUp(key)   && _kPrev.IsKeyDown(key);

    // -----------------------------------------------------------------------
    // Mouse helpers
    // -----------------------------------------------------------------------
    public bool IsMouseButtonDown(MouseButton btn)        => GetButtonState(_mCurr, btn) == ButtonState.Pressed;
    public bool IsMouseButtonJustPressed(MouseButton btn) => GetButtonState(_mCurr, btn) == ButtonState.Pressed
                                                           && GetButtonState(_mPrev, btn) == ButtonState.Released;

    private static ButtonState GetButtonState(MouseState ms, MouseButton btn) => btn switch
    {
        MouseButton.Left   => ms.LeftButton,
        MouseButton.Right  => ms.RightButton,
        MouseButton.Middle => ms.MiddleButton,
        _                  => ButtonState.Released
    };

    // -----------------------------------------------------------------------
    // Gamepad helpers
    // -----------------------------------------------------------------------
    public bool IsButtonDown(Buttons btn)        => _gCurr.IsButtonDown(btn);
    public bool IsButtonJustPressed(Buttons btn) => _gCurr.IsButtonDown(btn) && !_gPrev.IsButtonDown(btn);

    public Vector2 LeftStick  => _gCurr.ThumbSticks.Left;
    public Vector2 RightStick => _gCurr.ThumbSticks.Right;
    public float   LeftTrigger  => _gCurr.Triggers.Left;
    public float   RightTrigger => _gCurr.Triggers.Right;
}

/// <summary>Convenience enum for mouse buttons.</summary>
public enum MouseButton { Left, Right, Middle }

Use it in Game1.cs like this:

private readonly InputManager _input = new InputManager();

protected override void Update(GameTime gameTime)
{
    // Always update input first, before any other Update logic
    _input.Update();

    if (_input.IsKeyDown(Keys.Escape)) Exit();

    float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;
    float speed = 200f;

    // Keyboard or gamepad movement — whichever is active
    Vector2 move = Vector2.Zero;

    if (_input.IsKeyDown(Keys.A) || _input.IsKeyDown(Keys.Left))  move.X -= 1f;
    if (_input.IsKeyDown(Keys.D) || _input.IsKeyDown(Keys.Right)) move.X += 1f;
    if (_input.IsKeyDown(Keys.W) || _input.IsKeyDown(Keys.Up))    move.Y -= 1f;
    if (_input.IsKeyDown(Keys.S) || _input.IsKeyDown(Keys.Down))  move.Y += 1f;

    if (_input.GamePadConnected && _input.LeftStick.Length() > 0.15f)
        move = _input.LeftStick * new Vector2(1f, -1f); // invert Y

    if (move != Vector2.Zero)
    {
        move.Normalize(); // prevent diagonal speed boost
        _playerPosition += move * speed * delta;
    }

    // Jump on A button or Space — just pressed, not held
    if (_input.IsButtonJustPressed(Buttons.A) || _input.IsKeyJustPressed(Keys.Space))
        Jump();

    base.Update(gameTime);
}

💡 Normalise diagonal movement: if a player holds both W and D simultaneously, the combined vector has a length of √2 ≈ 1.41, making diagonal movement 41% faster than axis-aligned movement. Calling move.Normalize() fixes this.

Action Mapping

For games with rebindable controls, map abstract actions to keys rather than hard-coding keys everywhere. A simple dictionary approach:

using Microsoft.Xna.Framework.Input;
using System.Collections.Generic;

namespace MySpriteGame;

public enum GameAction
{
    MoveLeft, MoveRight, MoveUp, MoveDown,
    Jump, Attack, Pause
}

public class ActionMap
{
    private readonly Dictionary<GameAction, Keys> _keyBindings = new()
    {
        [GameAction.MoveLeft]  = Keys.A,
        [GameAction.MoveRight] = Keys.D,
        [GameAction.MoveUp]    = Keys.W,
        [GameAction.MoveDown]  = Keys.S,
        [GameAction.Jump]      = Keys.Space,
        [GameAction.Attack]    = Keys.J,
        [GameAction.Pause]     = Keys.Escape,
    };

    private readonly Dictionary<GameAction, Buttons> _padBindings = new()
    {
        [GameAction.Jump]   = Buttons.A,
        [GameAction.Attack] = Buttons.X,
        [GameAction.Pause]  = Buttons.Start,
    };

    public void Rebind(GameAction action, Keys newKey)
        => _keyBindings[action] = newKey;

    public bool IsActionDown(GameAction action, InputManager input)
    {
        bool keyHeld = _keyBindings.TryGetValue(action, out Keys key)
                       && input.IsKeyDown(key);
        bool padHeld = _padBindings.TryGetValue(action, out Buttons btn)
                       && input.IsButtonDown(btn);
        return keyHeld || padHeld;
    }

    public bool IsActionJustPressed(GameAction action, InputManager input)
    {
        bool keyPressed = _keyBindings.TryGetValue(action, out Keys key)
                          && input.IsKeyJustPressed(key);
        bool padPressed = _padBindings.TryGetValue(action, out Buttons btn)
                          && input.IsButtonJustPressed(btn);
        return keyPressed || padPressed;
    }
}

Next Steps

  • Extend InputManager with support for multiple gamepads using a PlayerIndex parameter
  • Persist key bindings to a JSON file so players can rebind controls across sessions
  • Add touch input support via TouchPanel.GetState() for mobile and tablet targets
  • Read the First Sprite Tutorial to put movement code into a working game