Table of Contents

Chapter 11: Input Management

Learn how to create an input management system to handle keyboard, mouse, and gamepad input, including state tracking between frames and creating a reusable framework for handling player input.

In Chapter 10, you learned how to handle input from various devices like keyboard, mouse, and gamepad. While checking if an input is currently down works well for continuous actions like movement, many game actions should only happen once when an input is first pressed; think firing a weapon or jumping. To handle these scenarios, we need to compare the current input state with the previous frame's state to detect when an input changes from up to down.

In this chapter you will:

  • Learn the difference between an input being down versus being pressed
  • Track input states between frames
  • Create a reusable input management system
  • Simplify handling input across multiple devices

Let's start by understanding the concept of input state changes and how we can detect them.

Understanding Input States

When handling input in games, there are two key scenarios we need to consider:

  • An input is being held down (like holding a movement key).
  • An input was just pressed for one frame (like pressing a jump button).

Let's look at the difference using keyboard input as an example. With our current implementation, we can check if a key is down using KeyboardState.IsKeyDown:

// Get the current state of keyboard input.
KeyboardState keyboardState = Keyboard.GetState();

// Check if the space key is down.
if (keyboardState.IsKeyDown(Keys.Space))
{
    // This runs EVERY frame the space key is held down
}

However, many game actions shouldn't repeat while a key is held. For instance, if the Space key makes your character jump, you probably don't want them to jump repeatedly just because the player is holding the key down. Instead, you want the jump to happen only on the first frame when Space is pressed.

To detect this "just pressed" state, we need to compare two states:

  1. Is the key down in the current frame?
  2. Was the key up in the previous frame?

If both conditions are true, we know the key was just pressed. If we were to modify the above code to track the previous keyboard state it would look something like this:

// Track the state of keyboard input during the previous frame.
private KeyboardState _previousKeyboardState;

protected override void Update(GameTime gameTime)
{
    // Get the current state of keyboard input.
    KeyboardState keyboardState = Keyboard.GetState();

    // Compare if the space key is down on the current frame but was up on the previous frame.
    if (keyboardState.IsKeyDown(Keys.Space) && _previousKeyboardState.IsKeyUp(Keys.Space))
    {
        // This will only run on the first frame Space is pressed and will not
        // happen again until it has been released and then pressed again.
    }

    // At the end of update, store the current state of keyboard input into the
    // previous state tracker.
    _previousKeyboardState = keyboardState;
    
    base.Update(gameTime);
}

This same concept applies to mouse buttons and gamepad input as well. Any time you need to detect a "just pressed" or "just released" state, you'll need to compare the current input state with the previous frame's state.

So far, we've only been working with our game within the Game1.cs file. This has been fine for the examples given. Overtime, as the game grows, we're going to have a more complex system setup with different scenes, and each scene will need a way to track the state of input over time. We could do this by creating a lot of variables in each scene to track this information, or we can use object-oriented design concepts to create a reusable InputManager class to simplify this for us.

Before we create the InputManager class, let's first create classes for the keyboard, mouse, and gamepad that encapsulates the information about those inputs which will then be exposed through the InputManager.

To get started, create a new directory called Input in the MonoGameLibrary project. We'll put all of our input related classes here.

The KeyboardInfo Class

Let's start our input management system by creating a class to handle keyboard input. The KeyboardInfo class will encapsulate all keyboard-related functionality, making it easier to:

  • Track current and previous keyboard states
  • Detect when keys are pressed or released
  • Check if keys are being held down

In the Input directory of the MonoGameLibrary project, add a new file named KeyboardInfo.cs with this initial structure:

using Microsoft.Xna.Framework.Input;

namespace MonoGameLibrary.Input;

public class KeyboardInfo { }

KeyboardInfo Properties

To detect changes in keyboard input between frames, we need to track both the previous and current keyboard states. Add these properties to the KeyboardInfo class:

/// <summary>
/// Gets the state of keyboard input during the previous update cycle.
/// </summary>
public KeyboardState PreviousState { get; private set; }

/// <summary>
/// Gets the state of keyboard input during the current input cycle.
/// </summary>
public KeyboardState CurrentState { get; private set; }
Note

These properties use a public getter but private setter pattern. This allows other parts of the game to read the keyboard states if needed, while ensuring only the KeyboardInfo class can update them.

KeyboardInfo Constructor

The KeyboardInfo class needs a constructor to initialize the keyboard states. Add this constructor:

/// <summary>
/// Creates a new KeyboardInfo 
/// </summary>
public KeyboardInfo()
{
    PreviousState = new KeyboardState();
    CurrentState = Keyboard.GetState();
}

The constructor:

  • Creates an empty state for PreviousState since there is no previous input yet
  • Gets the current keyboard state as our starting point for CurrentState

This initialization ensures we have valid states to compare against in the first frame of our game, preventing any potential null reference issues when checking for input changes.

KeyboardInfo Methods

The KeyboardInfo class needs methods both for updating states and checking key states. Let's start with our update method:

/// <summary>
/// Updates the state information about keyboard input.
/// </summary>
public void Update()
{
    PreviousState = CurrentState;
    CurrentState = Keyboard.GetState();
}
Note

Each time Update is called, the current state becomes the previous state, and we get a fresh current state. This creates our frame-to-frame comparison chain.

Next, we'll add methods to check various key states:

/// <summary>
/// Returns a value that indicates if the specified key is currently down.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the specified key is currently down; otherwise, false.</returns>
public bool IsKeyDown(Keys key)
{
    return CurrentState.IsKeyDown(key);
}

/// <summary>
/// Returns a value that indicates whether the specified key is currently up.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the specified key is currently up; otherwise, false.</returns>
public bool IsKeyUp(Keys key)
{
    return CurrentState.IsKeyUp(key);
}

/// <summary>
/// Returns a value that indicates if the specified key was just pressed on the current frame.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the specified key was just pressed on the current frame; otherwise, false.</returns>
public bool WasKeyJustPressed(Keys key)
{
    return CurrentState.IsKeyDown(key) && PreviousState.IsKeyUp(key);
}

/// <summary>
/// Returns a value that indicates if the specified key was just released on the current frame.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the specified key was just released on the current frame; otherwise, false.</returns>
public bool WasKeyJustReleased(Keys key)
{
    return CurrentState.IsKeyUp(key) && PreviousState.IsKeyDown(key);
}

These methods serve two distinct purposes. For checking continuous states:

  • IsKeyDown: Returns true as long as the specified key is being held down.
  • IsKeyUp: Returns true as long as the specified key is not being pressed.

And for detecting state changes:

  • WasKeyJustPressed: Returns true only on the frame when the specified key changes from up-to-down.
  • WasKeyJustReleased: Returns true only on the frame when the specified key changes from down-to-up.
Tip

Use continuous state checks (IsKeyDown/IsKeyUp) for actions that should repeat while a key is held, like movement. Use single-frame checks (WasKeyJustPressed/WasKeyJustReleased) for actions that should happen once per key press, like jumping or shooting.

That's it for the KeyboardInfo class, let's move on to mouse input next.

MouseButton Enum

Recall from the Mouse Input section of the previous chapter that the MouseState struct provides button states through properties rather than methods like IsButtonDown/IsButtonUp. To keep our input management API consistent across devices, we'll create a MouseButton enum that lets us reference mouse buttons in a similar way to how we use Keys for keyboard input and Buttons for gamepad input.

In the Input directory of the MonoGameLibrary project, add a new file named MouseButton.cs with the following code:

namespace MonoGameLibrary.Input;

public enum MouseButton
{
    Left,
    Middle,
    Right,
    XButton1,
    XButton2
}
Note

Each enum value corresponds directly to a button property in MouseState:

The MouseInfo Class

To manage mouse input effectively, we need to track both current and previous states, as well as provide easy access to mouse position, scroll wheel values, and button states. The MouseInfo class will encapsulate all of this functionality, making it easier to:

  • Track current and previous mouse states.
  • Track the mouse position.
  • Check the change in mouse position between frames and if it was moved.
  • Track scroll wheel changes.
  • Detect when mouse buttons are pressed or released
  • Check if mouse buttons are being held down

Let's create this class in the Input directory of the MonoGameLibrary project. Add a new file named MouseInfo.cs with the following initial structure:

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

namespace MonoGameLibrary.Input;

public class MouseInfo { }

MouseInfo Properties

The MouseInfo class needs properties to track both mouse states and provide easy access to common mouse information. Let's add these properties.

First, we need properties for tracking mouse states:

/// <summary>
/// The state of mouse input during the previous update cycle.
/// </summary>
public MouseState PreviousState { get; private set; }

/// <summary>
/// The state of mouse input during the current update cycle.
/// </summary>
public MouseState CurrentState { get; private set; }

Next, we'll add properties for handling cursor position:

/// <summary>
/// Gets or Sets the current position of the mouse cursor in screen space.
/// </summary>
public Point Position
{
    get => CurrentState.Position;
    set => SetPosition(value.X, value.Y);
}

/// <summary>
/// Gets or Sets the current x-coordinate position of the mouse cursor in screen space.
/// </summary>
public int X
{
    get => CurrentState.X;
    set => SetPosition(value, CurrentState.Y);
}

/// <summary>
/// Gets or Sets the current y-coordinate position of the mouse cursor in screen space.
/// </summary>
public int Y
{
    get => CurrentState.Y;
    set => SetPosition(CurrentState.X, value);
}
Note

The position properties use a SetPosition method that we'll implement later. This method will handle the actual cursor positioning on screen.

These properties provide different ways to work with the cursor position:

  • Position: Gets/sets the cursor position as a Point.
  • X: Gets/sets just the horizontal position.
  • Y: Gets/sets just the vertical position.

Next, we'll add properties for determining if the mouse cursor moved between game frames and if so how much:

/// <summary>
/// Gets the difference in the mouse cursor position between the previous and current frame.
/// </summary>
public Point PositionDelta => CurrentState.Position - PreviousState.Position;

/// <summary>
/// Gets the difference in the mouse cursor x-position between the previous and current frame.
/// </summary>
public int XDelta => CurrentState.X - PreviousState.X;

/// <summary>
/// Gets the difference in the mouse cursor y-position between the previous and current frame.
/// </summary>
public int YDelta => CurrentState.Y - PreviousState.Y;

/// <summary>
/// Gets a value that indicates if the mouse cursor moved between the previous and current frames.
/// </summary>
public bool WasMoved => PositionDelta != Point.Zero;

The properties provide different ways of detecting mouse movement between frames:

  • PositionDelta: Gets how much the cursor moved between frames as a Point.
  • XDelta: Gets how much the cursor moved horizontally between frames.
  • YDelta: Gets how much the cursor moved vertically between frames.
  • WasMoved: Indicates if the cursor moved between frames.

Finally, we'll add properties for handling the scroll wheel:

/// <summary>
/// Gets the cumulative value of the mouse scroll wheel since the start of the game.
/// </summary>
public int ScrollWheel => CurrentState.ScrollWheelValue;

/// <summary>
/// Gets the value of the scroll wheel between the previous and current frame.
/// </summary>
public int ScrollWheelDelta => CurrentState.ScrollWheelValue - PreviousState.ScrollWheelValue;

The scroll wheel properties serve different purposes:

  • ScrollWheel: Gets the total accumulated scroll value since game start.
  • ScrollWheelDelta: Gets the change in scroll value just in this frame.
Tip

Use ScrollWheelDelta when you need to respond to how much the user just scrolled, rather than tracking the total scroll amount.

MouseInfo Constructor

The MouseInfo class needs a constructor to initialize the mouse states. Add this constructor:

/// <summary>
/// Creates a new MouseInfo.
/// </summary>
public MouseInfo()
{
    PreviousState = new MouseState();
    CurrentState = Mouse.GetState();
}

The constructor:

  • Creates an empty state for PreviousState since there is no previous input yet.
  • Gets the current mouse state as our starting point for CurrentState.

This initialization ensures we have valid states to compare against in the first frame of our game, preventing any potential null reference issues when checking for input changes.

MouseInfo Methods

The MouseInfo class needs methods for updating states, checking button states, and setting the cursor position. Let's start with our update method:

/// <summary>
/// Updates the state information about mouse input.
/// </summary>
public void Update()
{
    PreviousState = CurrentState;
    CurrentState = Mouse.GetState();
}

Next, we'll add methods to check various button states:

/// <summary>
/// Returns a value that indicates whether the specified mouse button is currently down.
/// </summary>
/// <param name="button">The mouse button to check.</param>
/// <returns>true if the specified mouse button is currently down; otherwise, false.</returns>
public bool IsButtonDown(MouseButton button)
{
    switch (button)
    {
        case MouseButton.Left:
            return CurrentState.LeftButton == ButtonState.Pressed;
        case MouseButton.Middle:
            return CurrentState.MiddleButton == ButtonState.Pressed;
        case MouseButton.Right:
            return CurrentState.RightButton == ButtonState.Pressed;
        case MouseButton.XButton1:
            return CurrentState.XButton1 == ButtonState.Pressed;
        case MouseButton.XButton2:
            return CurrentState.XButton2 == ButtonState.Pressed;
        default:
            return false;
    }
}

/// <summary>
/// Returns a value that indicates whether the specified mouse button is current up.
/// </summary>
/// <param name="button">The mouse button to check.</param>
/// <returns>true if the specified mouse button is currently up; otherwise, false.</returns>
public bool IsButtonUp(MouseButton button)
{
    switch (button)
    {
        case MouseButton.Left:
            return CurrentState.LeftButton == ButtonState.Released;
        case MouseButton.Middle:
            return CurrentState.MiddleButton == ButtonState.Released;
        case MouseButton.Right:
            return CurrentState.RightButton == ButtonState.Released;
        case MouseButton.XButton1:
            return CurrentState.XButton1 == ButtonState.Released;
        case MouseButton.XButton2:
            return CurrentState.XButton2 == ButtonState.Released;
        default:
            return false;
    }
}

/// <summary>
/// Returns a value that indicates whether the specified mouse button was just pressed on the current frame.
/// </summary>
/// <param name="button">The mouse button to check.</param>
/// <returns>true if the specified mouse button was just pressed on the current frame; otherwise, false.</returns>
public bool WasButtonJustPressed(MouseButton button)
{
    switch (button)
    {
        case MouseButton.Left:
            return CurrentState.LeftButton == ButtonState.Pressed && PreviousState.LeftButton == ButtonState.Released;
        case MouseButton.Middle:
            return CurrentState.MiddleButton == ButtonState.Pressed && PreviousState.MiddleButton == ButtonState.Released;
        case MouseButton.Right:
            return CurrentState.RightButton == ButtonState.Pressed && PreviousState.RightButton == ButtonState.Released;
        case MouseButton.XButton1:
            return CurrentState.XButton1 == ButtonState.Pressed && PreviousState.XButton1 == ButtonState.Released;
        case MouseButton.XButton2:
            return CurrentState.XButton2 == ButtonState.Pressed && PreviousState.XButton2 == ButtonState.Released;
        default:
            return false;
    }
}

/// <summary>
/// Returns a value that indicates whether the specified mouse button was just released on the current frame.
/// </summary>
/// <param name="button">The mouse button to check.</param>
/// <returns>true if the specified mouse button was just released on the current frame; otherwise, false.</returns>F
public bool WasButtonJustReleased(MouseButton button)
{
    switch (button)
    {
        case MouseButton.Left:
            return CurrentState.LeftButton == ButtonState.Released && PreviousState.LeftButton == ButtonState.Pressed;
        case MouseButton.Middle:
            return CurrentState.MiddleButton == ButtonState.Released && PreviousState.MiddleButton == ButtonState.Pressed;
        case MouseButton.Right:
            return CurrentState.RightButton == ButtonState.Released && PreviousState.RightButton == ButtonState.Pressed;
        case MouseButton.XButton1:
            return CurrentState.XButton1 == ButtonState.Released && PreviousState.XButton1 == ButtonState.Pressed;
        case MouseButton.XButton2:
            return CurrentState.XButton2 == ButtonState.Released && PreviousState.XButton2 == ButtonState.Pressed;
        default:
            return false;
    }
}

These methods serve two distinct purposes. For checking continuous states:

  • IsButtonDown: Returns true as long as the specified button is being held down.
  • IsButtonUp: Returns true as long as the specified button is not being pressed.

And for detecting state changes:

  • WasButtonJustPressed: Returns true only on the frame when the specified button changes from up-to-down.
  • WasButtonJustReleased: Returns true only on the frame when the specified button changes from down-to-up.
Note

Each method uses a switch statement to check the appropriate button property from the MouseState based on which MouseButton enum value is provided. This provides a consistent API while handling the different button properties internally.

Finally, we need a method to handle setting the cursor position:

/// <summary>
/// Sets the current position of the mouse cursor in screen space and updates the CurrentState with the new position.
/// </summary>
/// <param name="x">The x-coordinate location of the mouse cursor in screen space.</param>
/// <param name="y">The y-coordinate location of the mouse cursor in screen space.</param>
public void SetPosition(int x, int y)
{
    Mouse.SetPosition(x, y);
    CurrentState = new MouseState(
        x,
        y,
        CurrentState.ScrollWheelValue,
        CurrentState.LeftButton,
        CurrentState.MiddleButton,
        CurrentState.RightButton,
        CurrentState.XButton1,
        CurrentState.XButton2
    );
}
Tip

Notice that after setting the position, we immediately update the CurrentState. This ensures our state tracking remains accurate even when manually moving the cursor.

That's it for the MouseInfo class, next we'll move onto gamepad input.

The GamePadInfo Class

To manage gamepad input effectively, we need to track both current and previous states, is the gamepad still connected, as well as provide easy access to the thumbstick values, trigger values, and button states. The GamePadInfo class will encapsulate all of this functionality, making it easier to:

  • Track current and previous gamepad states.
  • Check if the gamepad is still connected.
  • Track the position of the left and right thumbsticks.
  • Check the values of the left and right triggers.
  • Detect when gamepad buttons are pressed or released.
  • Check if gamepad buttons are being held down.
  • Start and Stop vibration of a gamepad.

Let's create this class in the Input directory of the MonoGameLibrary project. Add a new file named GamePadInfo.cs with the following initial structure:

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

namespace MonoGameLibrary.Input;

public class GamePadInfo { }

GamePadInfo Properties

We use vibration in gamepads to provide haptic feedback to the player. The GamePad class provides the SetVibration method to tell the gamepad to vibrate, but it does not provide a timing mechanism for it if we wanted to only vibrate for a certain period of time. Add the following private field to the GamePadInfo class:

private TimeSpan _vibrationTimeRemaining = TimeSpan.Zero;

Recall from the previous chapter that a PlayerIndex value needs to be supplied when calling Gamepad.GetState. Doing this returns the state of the gamepad connected at that player index. So we'll need a property to track the player index this gamepad info is for.

/// <summary>
/// Gets the index of the player this gamepad is for.
/// </summary>
public PlayerIndex PlayerIndex { get; }

To detect changes in the gamepad input between frames, we need to track both the previous and current gamepad states. Add these properties to the GamePadInfo class:

/// <summary>
/// Gets the state of input for this gamepad during the previous update cycle.
/// </summary>
public GamePadState PreviousState { get; private set; }

/// <summary>
/// Gets the state of input for this gamepad during the current update cycle.
/// </summary>
public GamePadState CurrentState { get; private set; }

There are times that a gamepad can disconnect for various reasons; being unplugged, bluetooth disconnection, or battery dying are just some examples. To track if the gamepad is connected, add the following property:

/// <summary>
/// Gets a value that indicates if this gamepad is currently connected.
/// </summary>
public bool IsConnected => CurrentState.IsConnected;

The values of the thumbsticks and triggers can be accessed through the CurrentState. However, instead of having to navigate through multiple property chains to get this information, add the following properties to get direct access to the values:

/// <summary>
/// Gets the value of the left thumbstick of this gamepad.
/// </summary>
public Vector2 LeftThumbStick => CurrentState.ThumbSticks.Left;

/// <summary>
/// Gets the value of the right thumbstick of this gamepad.
/// </summary>
public Vector2 RightThumbStick => CurrentState.ThumbSticks.Right;

/// <summary>
/// Gets the value of the left trigger of this gamepad.
/// </summary>
public float LeftTrigger => CurrentState.Triggers.Left;

/// <summary>
/// Gets the value of the right trigger of this gamepad.
/// </summary>
public float RightTrigger => CurrentState.Triggers.Right;

GamePadInfo Constructor

The GamePadInfo class needs a constructor to initialize the gamepad states. Add this constructor

/// <summary>
/// Creates a new GamePadInfo for the gamepad connected at the specified player index.
/// </summary>
/// <param name="playerIndex">The index of the player for this gamepad.</param>
public GamePadInfo(PlayerIndex playerIndex)
{
    PlayerIndex = playerIndex;
    PreviousState = new GamePadState();
    CurrentState = GamePad.GetState(playerIndex);
}

This constructor

  • Requires a PlayerIndex value which is stored and will be used to get the states for the correct gamepad
  • Creates an empty state for PreviousState since there is no previous state yet.
  • Gets the current gamepad state as our starting CurrentState.

This initialization ensures we have valid states to compare against in the first frame of our game, preventing any potential null reference issues when checking for input changes.

GamePadInfo Methods

The GamePadInfo class needs methods for updating states, checking button states, and controlling vibration. Let's start with our update method:

/// <summary>
/// Updates the state information for this gamepad input.
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
    PreviousState = CurrentState;
    CurrentState = GamePad.GetState(PlayerIndex);

    if (_vibrationTimeRemaining > TimeSpan.Zero)
    {
        _vibrationTimeRemaining -= gameTime.ElapsedGameTime;

        if (_vibrationTimeRemaining <= TimeSpan.Zero)
        {
            StopVibration();
        }
    }
}
Note

Unlike keyboard and mouse input, the gamepad update method takes a GameTime parameter. This allows us to track and manage timed vibration effects.

Next, we'll add methods to check various button states:

/// <summary>
/// Returns a value that indicates whether the specified gamepad button is current down.
/// </summary>
/// <param name="button">The gamepad button to check.</param>
/// <returns>true if the specified gamepad button is currently down; otherwise, false.</returns>
public bool IsButtonDown(Buttons button)
{
    return CurrentState.IsButtonDown(button);
}

/// <summary>
/// Returns a value that indicates whether the specified gamepad button is currently up.
/// </summary>
/// <param name="button">The gamepad button to check.</param>
/// <returns>true if the specified gamepad button is currently up; otherwise, false.</returns>
public bool IsButtonUp(Buttons button)
{
    return CurrentState.IsButtonUp(button);
}

/// <summary>
/// Returns a value that indicates whether the specified gamepad button was just pressed on the current frame.
/// </summary>
/// <param name="button"><The gamepad button to check./param>
/// <returns>true if the specified gamepad button was just pressed on the current frame; otherwise, false.</returns>
public bool WasButtonJustPressed(Buttons button)
{
    return CurrentState.IsButtonDown(button) && PreviousState.IsButtonUp(button);
}

/// <summary>
/// Returns a value that indicates whether the specified gamepad button was just released on the current frame.
/// </summary>
/// <param name="button"><The gamepad button to check./param>
/// <returns>true if the specified gamepad button was just released on the current frame; otherwise, false.</returns>
public bool WasButtonJustReleased(Buttons button)
{
    return CurrentState.IsButtonUp(button) && PreviousState.IsButtonDown(button);
}

These methods serve two distinct purposes. For checking continuous states:

  • IsButtonDown: Returns true as long as a button is being held down.
  • IsButtonUp: Returns true as long as a button is not being pressed.

And for detecting state changes:

  • WasButtonJustPressed: Returns true only on the frame when a button changes from up-to-down.
  • WasButtonJustReleased: Returns true only on the frame when a button changes from down-to-up.

Finally, we'll add methods for controlling gamepad vibration:

/// <summary>
/// Sets the vibration for all motors of this gamepad.
/// </summary>
/// <param name="strength">The strength of the vibration from 0.0f (none) to 1.0f (full).</param>
/// <param name="time">The amount of time the vibration should occur.</param>
public void SetVibration(float strength, TimeSpan time)
{
    _vibrationTimeRemaining = time;
    GamePad.SetVibration(PlayerIndex, strength, strength);
}

/// <summary>
/// Stops the vibration of all motors for this gamepad.
/// </summary>
public void StopVibration()
{
    GamePad.SetVibration(PlayerIndex, 0.0f, 0.0f);
}

The vibration methods provide control over the gamepad's haptic feedback:

  • SetVibration: Starts vibration at the specified strength for a set duration.
  • StopVibration: Immediately stops all vibration.
Tip

When setting vibration, you can specify both the strength (0.0f to 1.0f) and duration. The vibration will automatically stop after the specified time has elapsed, so you don't need to manage stopping it manually.

That's it for the GamePadInfo class. Next, let's create the actual input manager.

The InputManager Class

Now that we have classes to handle keyboard, mouse, and gamepad input individually, we can create a centralized manager class to coordinate all input handling.

In the Input directory of the MonoGameLibrary project, add a new file named InputManager.cs with this initial structure:

using Microsoft.Xna.Framework;

namespace MonoGameLibrary.Input;

public class InputManager
{

}

InputManager Properties

The InputManager class needs properties to access each type of input device. Add these properties:

/// <summary>
/// Gets the state information of keyboard input.
/// </summary>
public KeyboardInfo Keyboard { get; private set; }

/// <summary>
/// Gets the state information of mouse input.
/// </summary>
public MouseInfo Mouse { get; private set; }

/// <summary>
/// Gets the state information of a gamepad.
/// </summary>
public GamePadInfo[] GamePads { get; private set; }
Note

The GamePads property is an array because MonoGame supports up to four gamepads simultaneously. Each gamepad is associated with a PlayerIndex (0-3).

InputManager Constructor

The constructor for the InputManager initializes the keybaord, mouse, and gamepad states. Add the following constructor:

/// <summary>
/// Creates a new InputManager.
/// </summary>
/// <param name="game">The game this input manager belongs to.</param>
public InputManager()
{
    Keyboard = new KeyboardInfo();
    Mouse = new MouseInfo();

    GamePads = new GamePadInfo[4];
    for (int i = 0; i < 4; i++)
    {
        GamePads[i] = new GamePadInfo((PlayerIndex)i);
    }
}

InputManager Methods

The Update method for the InputManager calls update for each device so that they can update their internal states.

/// <summary>
/// Updates the state information for the keyboard, mouse, and gamepad inputs.
/// </summary>
/// <param name="gameTime">A snapshot of the timing values for the current frame.</param>
public void Update(GameTime gameTime)
{
    Keyboard.Update();
    Mouse.Update();

    for (int i = 0; i < 4; i++)
    {
        GamePads[i].Update(gameTime);
    }
}

Implementing the InputManager Class

Now tha we have our input management system complete, let's update our game to use it. We'll do this in two steps:

  1. First, update the Core class to add the InputManager globally.
  2. Update the Game1 class to use the global input manager from Core.

Updating the Core Class

The Core class serves as our base game class, so we will update it to add and expose the InputManager globally. Open the Core.cs file in the MonoGameLibrary project and update it to the following:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary.Input;

namespace MonoGameLibrary;

public class Core : Game
{
    internal static Core s_instance;

    /// <summary>
    /// Gets a reference to the Core instance.
    /// </summary>
    public static Core Instance => s_instance;

    /// <summary>
    /// Gets the graphics device manager to control the presentation of graphics.
    /// </summary>
    public static GraphicsDeviceManager Graphics { get; private set; }

    /// <summary>
    /// Gets the graphics device used to create graphical resources and perform primitive rendering.
    /// </summary>
    public static new GraphicsDevice GraphicsDevice { get; private set; }

    /// <summary>
    /// Gets the sprite batch used for all 2D rendering.
    /// </summary>
    public static SpriteBatch SpriteBatch { get; private set; }

    /// <summary>
    /// Gets the content manager used to load global assets.
    /// </summary>
    public static new ContentManager Content { get; private set; }

    /// <summary>
    /// Gets a reference to to the input management system.
    /// </summary>
    public static InputManager Input { get; private set; }

    /// <summary>
    /// Gets or Sets a value that indicates if the game should exit when the esc key on the keyboard is pressed.
    /// </summary>
    public static bool ExitOnEscape { get; set; }

    /// <summary>
    /// Creates a new Core instance.
    /// </summary>
    /// <param name="title">The title to display in the title bar of the game window.</param>
    /// <param name="width">The initial width, in pixels, of the game window.</param>
    /// <param name="height">The initial height, in pixels, of the game window.</param>
    /// <param name="fullScreen">Indicates if the game should start in fullscreen mode.</param>
    public Core(string title, int width, int height, bool fullScreen)
    {
        // Ensure that multiple cores are not created.
        if (s_instance != null)
        {
            throw new InvalidOperationException($"Only a single Core instance can be created");
        }

        // Store reference to engine for global member access.
        s_instance = this;

        // Create a new graphics device manager.
        Graphics = new GraphicsDeviceManager(this);

        // Set the graphics defaults
        Graphics.PreferredBackBufferWidth = width;
        Graphics.PreferredBackBufferHeight = height;
        Graphics.IsFullScreen = fullScreen;

        // Apply the graphic presentation changes
        Graphics.ApplyChanges();

        // Set the window title
        Window.Title = title;

        // Set the core's content manager to a reference of hte base Game's
        // content manager.
        Content = base.Content;

        // Set the root directory for content
        Content.RootDirectory = "Content";

        // Mouse is visible by default
        IsMouseVisible = true;
    }

    protected override void Initialize()
    {
        base.Initialize();

        // Set the core's graphics device to a reference of the base Game's
        // graphics device.
        GraphicsDevice = base.GraphicsDevice;

        // Create the sprite batch instance.
        SpriteBatch = new SpriteBatch(GraphicsDevice);

        // Create a new input manager
        Input = new InputManager();
    }

    protected override void Update(GameTime gameTime)
    {
        // Update the input manager
        Input.Update(gameTime);

        if (ExitOnEscape && Input.Keyboard.IsKeyDown(Keys.Escape))
        {
            Exit();
        }

        base.Update(gameTime);
    }
}

The key changes to the Core class are:

  1. Added the using MonoGameLibrary.Input; directive to access the InputManager class.
  2. Added a static Input property to provide global access to the input manager.
  3. Added a static ExitOnEscape property to set whether the game should exit when the Escape key on the keyboard is pressed.
  4. In Initialize the input manager is created.
  5. Added an override for the Update method where:
    1. The input manager is updated
    2. A check is made to see if ExitOnEscape is true and if the Escape keyboard key is pressed.

Updating the Game1 Class

Now let's update our Game1 class to use the new input management system through the Core class. Open Game1.cs in the game project and update it to the following:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;
using MonoGameLibrary.Input;

namespace DungeonSlime;

public class Game1 : Core
{
    // Defines the slime animated sprite.
    private AnimatedSprite _slime;

    // Defines the bat animated sprite.
    private AnimatedSprite _bat;

    // Tracks the position of the slime.
    private Vector2 _slimePosition;

    // Speed multiplier when moving.
    private const float MOVEMENT_SPEED = 5.0f;

    public Game1() : base("Dungeon Slime", 1280, 720, false)
    {

    }

    protected override void Initialize()
    {
        // TODO: Add your initialization logic here

        base.Initialize();
    }

    protected override void LoadContent()
    {
        // Create the texture atlas from the XML configuration file
        TextureAtlas atlas = TextureAtlas.FromFile(Content, "images/atlas-definition.xml");

        // Create the slime animated sprite from the atlas.
        _slime = atlas.CreateAnimatedSprite("slime-animation");

        // Create the bat animated sprite from the atlas.
        _bat = atlas.CreateAnimatedSprite("bat-animation");

        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        // Update the slime animated sprite.
        _slime.Update(gameTime);

        // Update the bat animated sprite.
        _bat.Update(gameTime);

        // Check for keyboard input and handle it.
        CheckKeyboardInput();

        // Check for gamepad input and handle it.
        CheckGamePadInput();

        base.Update(gameTime);
    }

    private void CheckKeyboardInput()
    {
        // If the space key is held down, the movement speed increases by 1.5
        float speed = MOVEMENT_SPEED;
        if (Input.Keyboard.IsKeyDown(Keys.Space))
        {
            speed *= 1.5f;
        }

        // If the W or Up keys are down, move the slime up on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.W) || Input.Keyboard.IsKeyDown(Keys.Up))
        {
            _slimePosition.Y -= speed;
        }

        // if the S or Down keys are down, move the slime down on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.S) || Input.Keyboard.IsKeyDown(Keys.Down))
        {
            _slimePosition.Y += speed;
        }

        // If the A or Left keys are down, move the slime left on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.A) || Input.Keyboard.IsKeyDown(Keys.Left))
        {
            _slimePosition.X -= speed;
        }

        // If the D or Right keys are down, move the slime right on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.D) || Input.Keyboard.IsKeyDown(Keys.Right))
        {
            _slimePosition.X += speed;
        }
    }

    private void CheckGamePadInput()
    {
        GamePadInfo gamePadOne = Input.GamePads[(int)PlayerIndex.One];

        // If the A button is held down, the movement speed increases by 1.5
        // and the gamepad vibrates as feedback to the player.
        float speed = MOVEMENT_SPEED;
        if (gamePadOne.IsButtonDown(Buttons.A))
        {
            speed *= 1.5f;
            GamePad.SetVibration(PlayerIndex.One, 1.0f, 1.0f);
        }
        else
        {
            GamePad.SetVibration(PlayerIndex.One, 0.0f, 0.0f);
        }

        // Check thumbstick first since it has priority over which gamepad input
        // is movement.  It has priority since the thumbstick values provide a
        // more granular analog value that can be used for movement.
        if (gamePadOne.LeftThumbStick != Vector2.Zero)
        {
            _slimePosition.X += gamePadOne.LeftThumbStick.X * speed;
            _slimePosition.Y -= gamePadOne.LeftThumbStick.Y * speed;
        }
        else
        {
            // If DPadUp is down, move the slime up on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadUp))
            {
                _slimePosition.Y -= speed;
            }

            // If DPadDown is down, move the slime down on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadDown))
            {
                _slimePosition.Y += speed;
            }

            // If DPapLeft is down, move the slime left on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadLeft))
            {
                _slimePosition.X -= speed;
            }

            // If DPadRight is down, move the slime right on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadRight))
            {
                _slimePosition.X += speed;
            }
        }
    }

    protected override void Draw(GameTime gameTime)
    {
        // Clear the back buffer.
        GraphicsDevice.Clear(Color.CornflowerBlue);

        // Begin the sprite batch to prepare for rendering.
        SpriteBatch.Begin(samplerState: SamplerState.PointClamp);

        // Draw the slime sprite.
        _slime.Draw(SpriteBatch, _slimePosition);

        // Draw the bat sprite 10px to the right of the slime.
        _bat.Draw(SpriteBatch, new Vector2(_slime.Width + 10, 0));

        // Always end the sprite batch when finished.
        SpriteBatch.End();

        base.Draw(gameTime);
    }
}

The key changes to the Game1 class are:

  1. In Update, the check for the gamepad back button or keyboard escape key being pressed was removed. This is now handled by the ExitOnEscape property and the Update method of the Core class.
  2. In CheckKeyboardInput and CheckGamepadInput, instead of getting the keyboard and gamepad states and then using the states, calls to check those devices are now done through the input.

Running the game now, you will be able to control it the same as before, only now we're using our new InputManager class instead.

Figure 11-1: The slime moving around based on device input

Conclusion

In this chapter, you learned how to:

  • Detect the difference between continuous and single-frame input states.
  • Create classes to manage different input devices.
  • Build a centralized InputManager to coordinate all input handling that is:
    • Reusable across different game projects
    • Easy to maintain and extend
    • Consistent across different input devices
  • Integrate the input system into the Core class for global access.
  • Update the game to use the new input management system.

Test Your Knowledge

  1. What's the difference between checking if an input is "down" versus checking if it was "just pressed"?

    "Down" checks if an input is currently being held, returning true every frame while held. "Just pressed" only returns true on the first frame when the input changes from up to down, requiring comparison between current and previous states.

  2. Why do we track both current and previous input states?

    Tracking both states allows us to detect when input changes occur by comparing the current frame's state with the previous frame's state. This is essential for implementing "just pressed" and "just released" checks.

  3. What advantage does the InputManager provide over handling input directly?

    The InputManager centralizes all input handling, automatically tracks states between frames, and provides a consistent API across different input devices. This makes the code more organized, reusable, and easier to maintain.