Interaction & UI
Advanced Input Handling
Once you can poll keyboard, mouse, and gamepad state, the next step is to stop scattering raw input checks throughout your game. This tutorial builds a reusable input layer that remembers previous state, exposes one-frame events, maps physical controls to game actions, combines keyboard and gamepad movement, handles thumbstick dead zones, and uses event-based text entry for typing.
Download the runnable sample project
Use the verified ZIP bundle for this article to review the extracted example files, run the sample host, and repackage the sources without bin or obj folders.
Reusable input state
Polling is still the foundation, but a small helper can centralise the pattern. Store current and previous KeyboardState, MouseState, and GamePadState snapshots once per frame, then ask higher-level questions such as WasKeyPressed(Keys.Space), IsKeyHeld(Keys.A), WasMouseClicked(MouseButton.Left), and WasButtonPressed(Buttons.A).
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
public enum MouseButton
{
Left,
Middle,
Right
}
public sealed class InputState
{
public KeyboardState CurrentKeyboard { get; private set; }
public KeyboardState PreviousKeyboard { get; private set; }
public MouseState CurrentMouse { get; private set; }
public MouseState PreviousMouse { get; private set; }
public GamePadState CurrentGamePad { get; private set; }
public GamePadState PreviousGamePad { get; private set; }
public void Advance(KeyboardState keyboard, MouseState mouse, GamePadState gamePad)
{
PreviousKeyboard = CurrentKeyboard;
PreviousMouse = CurrentMouse;
PreviousGamePad = CurrentGamePad;
CurrentKeyboard = keyboard;
CurrentMouse = mouse;
CurrentGamePad = gamePad;
}
}
public static class InputManager
{
private static readonly InputState State = new InputState();
public static void Update(PlayerIndex player)
{
State.Advance(
Keyboard.GetState(),
Mouse.GetState(),
GamePad.GetState(player, GamePadDeadZone.Circular));
}
public static bool WasKeyPressed(Keys key)
{
return State.CurrentKeyboard.IsKeyDown(key) && State.PreviousKeyboard.IsKeyUp(key);
}
public static bool IsKeyHeld(Keys key)
{
return State.CurrentKeyboard.IsKeyDown(key);
}
public static bool WasMouseClicked(MouseButton button)
{
return GetButton(State.CurrentMouse, button) == ButtonState.Pressed &&
GetButton(State.PreviousMouse, button) == ButtonState.Released;
}
public static int GetScrollDelta()
{
return State.CurrentMouse.ScrollWheelValue - State.PreviousMouse.ScrollWheelValue;
}
public static bool WasButtonPressed(Buttons button)
{
return State.CurrentGamePad.IsConnected &&
State.CurrentGamePad.IsButtonDown(button) &&
State.PreviousGamePad.IsButtonUp(button);
}
public static bool IsButtonHeld(Buttons button)
{
return State.CurrentGamePad.IsConnected && State.CurrentGamePad.IsButtonDown(button);
}
private static ButtonState GetButton(MouseState mouse, MouseButton button)
{
return button switch
{
MouseButton.Left => mouse.LeftButton,
MouseButton.Middle => mouse.MiddleButton,
MouseButton.Right => mouse.RightButton,
_ => ButtonState.Released
};
}
}
⚠️ Caution: Call InputManager.Update exactly once near the start of Update. Calling it twice in one frame overwrites the previous snapshot and makes single-press checks unreliable.
Action mapping and rebinding
Raw keys describe hardware. Game actions describe intent. A Dictionary<GameAction, Keys> lets gameplay ask for GameAction.Jump or GameAction.Fire without caring whether the current binding is Space, Left Control, or a player-selected key. The same idea can be extended with a second map for gamepad buttons.
using System.Collections.Generic;
using Microsoft.Xna.Framework.Input;
public enum GameAction
{
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
Jump,
Fire
}
private readonly Dictionary<GameAction, Keys> _keyBindings = new Dictionary<GameAction, Keys>
{
[GameAction.MoveLeft] = Keys.A,
[GameAction.MoveRight] = Keys.D,
[GameAction.MoveUp] = Keys.W,
[GameAction.MoveDown] = Keys.S,
[GameAction.Jump] = Keys.Space,
[GameAction.Fire] = Keys.LeftControl
};
public void Rebind(GameAction action, Keys newKey)
{
_keyBindings[action] = newKey;
}
public bool IsActionHeld(GameAction action)
{
return _keyBindings.TryGetValue(action, out Keys key) && InputManager.IsKeyHeld(key);
}
public bool WasActionPressed(GameAction action)
{
return _keyBindings.TryGetValue(action, out Keys key) && InputManager.WasKeyPressed(key);
}
💡 Store bindings in your settings file when your game has a real options screen. Keep the in-memory map simple first, then add saving and loading after the controls feel good.
Keyboard and gamepad virtual axes
A virtual axis returns one movement vector no matter which device is used. Keyboard actions produce digital directions such as left or up; the gamepad left thumbstick contributes an analogue vector. Normalise the keyboard vector so diagonal movement is not faster, then prefer the strongest device input for the current frame.
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
public static Vector2 GetMovement(Dictionary<GameAction, Keys> keyBindings)
{
Vector2 movement = Vector2.Zero;
if (IsActionHeld(GameAction.MoveLeft, keyBindings)) movement.X -= 1f;
if (IsActionHeld(GameAction.MoveRight, keyBindings)) movement.X += 1f;
if (IsActionHeld(GameAction.MoveUp, keyBindings)) movement.Y -= 1f;
if (IsActionHeld(GameAction.MoveDown, keyBindings)) movement.Y += 1f;
if (movement != Vector2.Zero)
movement.Normalize();
GamePadState pad = GamePad.GetState(PlayerIndex.One, GamePadDeadZone.Circular);
if (pad.IsConnected)
{
Vector2 stick = pad.ThumbSticks.Left;
stick.Y = -stick.Y;
if (stick.LengthSquared() > movement.LengthSquared())
movement = stick;
}
return movement;
}
Thumbstick dead zones
Analogue sticks rarely rest at exactly zero. A dead zone ignores tiny values so the player does not drift. MonoGame can apply this for you with GamePad.GetState(PlayerIndex.One, GamePadDeadZone.Circular). Circular dead zones are usually a good default for movement because the stick behaves consistently in every direction.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
GamePadState circular = GamePad.GetState(PlayerIndex.One, GamePadDeadZone.Circular);
Vector2 movement = circular.ThumbSticks.Left;
movement.Y = -movement.Y;
private static Vector2 ApplyCircularDeadZone(Vector2 value, float deadZone)
{
float length = value.Length();
if (length <= deadZone)
return Vector2.Zero;
float scaledLength = MathHelper.Clamp((length - deadZone) / (1f - deadZone), 0f, 1f);
return Vector2.Normalize(value) * scaledLength;
}
⚠️ Warning: Avoid square dead zones for character movement unless you specifically want uneven diagonals. They can make diagonal input feel stronger or weaker than horizontal input.
Input buffering and simple combos
For responsive action games, keep a short list of recent input events with timestamps. A jump buffer might remember that GameAction.Jump was pressed for 0.12 seconds so it still triggers when the player lands a few frames later. Simple combos work the same way: record actions such as left, right, fire, then compare the recent sequence within a small time window.
💡 Keep buffers short and action-based. Buffer GameAction.Fire, not Keys.Space, so rebinding and gamepad input continue to work.
⚠️ Caution: Do not buffer held movement every frame. Buffer discrete edges such as pressed, released, clicked, or confirmed.
Text entry uses events
Typing names, chat messages, or console commands is not the same as gameplay input. Use Window.TextInput for text because it receives characters after the operating system has handled keyboard layout, shift state, and repeated characters. Polling keys is suitable for controls, not for typing sentences.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System.Text;
private readonly StringBuilder _typedName = new StringBuilder();
protected override void Initialize()
{
Window.TextInput += OnTextInput;
base.Initialize();
}
private void OnTextInput(object sender, TextInputEventArgs e)
{
if (e.Key == Keys.Back && _typedName.Length > 0)
{
_typedName.Length--;
return;
}
if (!char.IsControl(e.Character))
_typedName.Append(e.Character);
}
⚠️ Warning: Do not build text fields by polling KeyboardState. Polling cannot correctly represent keyboard layouts, repeated text, punctuation, or composed characters.
Complete example: rebindable movement
This complete Game1.cs generates a 1x1 texture in code, stretches it into a box, and moves it with either keyboard actions or the left thumbstick. Press F1/F2 to rebind left movement between Left Arrow and A, press F3/F4 to rebind right movement between Right Arrow and D, press R to reset bindings, and press Space or gamepad A to flash the box.
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Game1 : Game
{
private readonly GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
private Texture2D _pixel;
private readonly Dictionary<GameAction, Keys> _keyBindings = new Dictionary<GameAction, Keys>();
private readonly Dictionary<GameAction, Buttons> _buttonBindings = new Dictionary<GameAction, Buttons>();
private Vector2 _boxPosition = new Vector2(420, 240);
private float _flashTimer;
private const int BoxSize = 56;
private const float MoveSpeed = 320f;
public Game1()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
}
protected override void Initialize()
{
_graphics.PreferredBackBufferWidth = 960;
_graphics.PreferredBackBufferHeight = 540;
_graphics.ApplyChanges();
ResetBindings();
base.Initialize();
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_pixel = new Texture2D(GraphicsDevice, 1, 1);
_pixel.SetData(new[] { Color.White });
}
protected override void Update(GameTime gameTime)
{
InputManager.Update(PlayerIndex.One);
if (InputManager.WasKeyPressed(Keys.Escape))
Exit();
if (InputManager.WasKeyPressed(Keys.R))
ResetBindings();
if (InputManager.WasKeyPressed(Keys.F1))
_keyBindings[GameAction.MoveLeft] = Keys.Left;
if (InputManager.WasKeyPressed(Keys.F2))
_keyBindings[GameAction.MoveLeft] = Keys.A;
if (InputManager.WasKeyPressed(Keys.F3))
_keyBindings[GameAction.MoveRight] = Keys.Right;
if (InputManager.WasKeyPressed(Keys.F4))
_keyBindings[GameAction.MoveRight] = Keys.D;
if (InputManager.WasActionPressed(GameAction.Fire, _keyBindings, _buttonBindings))
_flashTimer = 0.18f;
float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
Vector2 movement = InputManager.GetMovement(_keyBindings);
_boxPosition += movement * MoveSpeed * dt;
_flashTimer = MathHelper.Max(0f, _flashTimer - dt);
ClampBoxToWindow();
base.Update(gameTime);
}
private void ResetBindings()
{
_keyBindings[GameAction.MoveLeft] = Keys.A;
_keyBindings[GameAction.MoveRight] = Keys.D;
_keyBindings[GameAction.MoveUp] = Keys.W;
_keyBindings[GameAction.MoveDown] = Keys.S;
_keyBindings[GameAction.Fire] = Keys.Space;
_buttonBindings[GameAction.Fire] = Buttons.A;
}
private void ClampBoxToWindow()
{
Rectangle bounds = GraphicsDevice.Viewport.Bounds;
_boxPosition.X = MathHelper.Clamp(_boxPosition.X, 0f, bounds.Width - BoxSize);
_boxPosition.Y = MathHelper.Clamp(_boxPosition.Y, 0f, bounds.Height - BoxSize);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
Color boxColour = _flashTimer > 0f ? Color.Gold : Color.OrangeRed;
Rectangle box = new Rectangle((int)_boxPosition.X, (int)_boxPosition.Y, BoxSize, BoxSize);
_spriteBatch.Begin();
_spriteBatch.Draw(_pixel, box, boxColour);
_spriteBatch.End();
base.Draw(gameTime);
}
}
public enum GameAction
{
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
Fire
}
public sealed class InputState
{
public KeyboardState CurrentKeyboard { get; private set; }
public KeyboardState PreviousKeyboard { get; private set; }
public MouseState CurrentMouse { get; private set; }
public MouseState PreviousMouse { get; private set; }
public GamePadState CurrentGamePad { get; private set; }
public GamePadState PreviousGamePad { get; private set; }
public void Advance(KeyboardState keyboard, MouseState mouse, GamePadState gamePad)
{
PreviousKeyboard = CurrentKeyboard;
PreviousMouse = CurrentMouse;
PreviousGamePad = CurrentGamePad;
CurrentKeyboard = keyboard;
CurrentMouse = mouse;
CurrentGamePad = gamePad;
}
}
public static class InputManager
{
private static readonly InputState State = new InputState();
public static void Update(PlayerIndex player)
{
State.Advance(
Keyboard.GetState(),
Mouse.GetState(),
GamePad.GetState(player, GamePadDeadZone.Circular));
}
public static bool WasKeyPressed(Keys key)
{
return State.CurrentKeyboard.IsKeyDown(key) && State.PreviousKeyboard.IsKeyUp(key);
}
public static bool IsKeyHeld(Keys key)
{
return State.CurrentKeyboard.IsKeyDown(key);
}
public static bool WasActionPressed(
GameAction action,
Dictionary<GameAction, Keys> keyBindings,
Dictionary<GameAction, Buttons> buttonBindings)
{
bool keyPressed = keyBindings.TryGetValue(action, out Keys key) && WasKeyPressed(key);
bool buttonPressed = buttonBindings.TryGetValue(action, out Buttons button) && WasButtonPressed(button);
return keyPressed || buttonPressed;
}
public static bool IsActionHeld(GameAction action, Dictionary<GameAction, Keys> keyBindings)
{
return keyBindings.TryGetValue(action, out Keys key) && IsKeyHeld(key);
}
public static Vector2 GetMovement(Dictionary<GameAction, Keys> keyBindings)
{
Vector2 movement = Vector2.Zero;
if (IsActionHeld(GameAction.MoveLeft, keyBindings)) movement.X -= 1f;
if (IsActionHeld(GameAction.MoveRight, keyBindings)) movement.X += 1f;
if (IsActionHeld(GameAction.MoveUp, keyBindings)) movement.Y -= 1f;
if (IsActionHeld(GameAction.MoveDown, keyBindings)) movement.Y += 1f;
if (movement != Vector2.Zero)
movement.Normalize();
if (State.CurrentGamePad.IsConnected)
{
Vector2 stick = State.CurrentGamePad.ThumbSticks.Left;
stick.Y = -stick.Y;
if (stick.LengthSquared() > 1f)
stick.Normalize();
if (stick.LengthSquared() > movement.LengthSquared())
movement = stick;
}
return movement;
}
private static bool WasButtonPressed(Buttons button)
{
return State.CurrentGamePad.IsConnected &&
State.CurrentGamePad.IsButtonDown(button) &&
State.PreviousGamePad.IsButtonUp(button);
}
}
Run it with dotnet run. Move with WASD or the left thumbstick, rebind horizontal movement with the function keys, and press Escape to quit.
dotnet build
dotnet run
Common pitfalls
- Mixing raw keys into gameplay after adding actions. Keep gameplay on
GameActionchecks so rebinding remains complete. - Polling text entry. Use
Window.TextInputfor typing and polling for gameplay controls. - Forgetting gamepad dead zones. Small resting values can move the player without touching the stick.
- Normalising a zero vector. Check for
Vector2.Zerobefore callingNormalize. - Buffering hardware inputs instead of actions. Action buffers are portable across keyboards, controllers, and rebinding.
Next steps
🎓 Test Your Knowledge
Check your understanding of the key concepts from this tutorial.
What is an “input action mapping” or “virtual input” approach?
✔ Correct!
✘ Not quite.
An InputManager converts physical state to logical actions: if (_actions["Jump"].IsPressed()) { ... }
What is a “dead zone” for analogue sticks?
✔ Correct!
✘ Not quite.
float len = stick.Length(); if (len < deadZone) stick = Vector2.Zero; else stick = Vector2.Normalize(stick) * ((len - deadZone) / (1f - deadZone));
What benefit does buffering a jump input provide?
✔ Correct!
✘ Not quite.
Store a _jumpBuffer timer; when the buffer is > 0 and the player is grounded, execute the jump.
How should you handle controller disconnect events?
✔ Correct!
✘ Not quite.
Poll GamePad.GetState(pi).IsConnected each Update and respond when it changes to false.