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:
- Is the key down in the current frame?
- 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:
Left
: Maps to MouseState.LeftButton.Middle
: Maps to MouseState.MiddleButton.Right
: Maps to MouseState.RightButton.XButton1
: Maps to MouseState.XButton1.XButton2
: Maps to MouseState.XButton2.
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:
- First, update the
Core
class to add theInputManager
globally. - Update the
Game1
class to use the global input manager fromCore
.
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:
- Added the
using MonoGameLibrary.Input;
directive to access theInputManager
class. - Added a static
Input
property to provide global access to the input manager. - Added a static
ExitOnEscape
property to set whether the game should exit when the Escape key on the keyboard is pressed. - In
Initialize
the input manager is created. - Added an override for the
Update
method where:- The input manager is updated
- 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:
- 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 theUpdate
method of theCore
class. - In
CheckKeyboardInput
andCheckGamepadInput
, 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
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.
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.
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.