Week 1 - Advanced Input

Start Project: Week1Start.zip

Finish Project: Week1Finish.zip

Notes:

  • I'm Jared Robertson.
    • I'm a fellow student at Utah State University. I plan on graduating this semester with my Master's Degree in Computer Science.
    • This is my third semester doing SIGXNA.
    • I love programming and especially love making games.
  • If you ever have any questions, requests for tutorials, or anything else, feel free to email me. My email address is <my first name>.<my last name>@aggiemail…
  • These tutorials are a little more advanced than what we did last semester. If you want to get caught up to a point where you can understand everything that these tutorials cover, last semester's tutorials will help a lot. If you're just starting to learn XNA, RB Whitaker has some excellent tutorials that will quickly turn you into an XNA master.

Alright, now that we have all that out of the way, let's get started on this tutorial.
Download Week1Start.zip, unzip it, and open the project in Visual Studio. The only file we'll be making changes to is Game1.cs, so go ahead and open it.

Take a minute to play the game and get a feeling for what's going on.

Scroll down to the Update() method and take a look at it.
You'll notice that most of the code is checking keyboardState.IsKeyDown(…) and previousKeyboardState.IsKeyUp(…). keyboardState looks for hard-coded keys to determine if it should perform an action. What if we want to change the keys? Using keyboardState how it is right now, that's not possible. Wouldn't it be better to check if the action should be performed and not if a key is down or up?
Take a look at the fire command by the comment that says // If the player presses space, fire the gun. If you try playing the game, you'll notice that every time you press the spacebar to fire, it only fires once and you'll have to press the spacebar again to fire again. This will quickly destroy the keyboard and won't be much fun for the player. Wouldn't it be better to have it "cool down" and fire automatically a few times a second?
We'll fix these things using a class called KeyboardManager instead of keyboardState. KeyboardManager handles keyboard input by allowing the programmer to register keyboard keys with actions (like turn-left or fire bullet) and then instead of checking to see if a key is pressed or not, it checks to see if the action should be performed. This will make more sense when we write it out in code.

Let's start by getting rid of keyboardState and all its usages. Near the top of the file, there are the declarations of keyboardState and previousKeyboardState.

protected KeyboardState previousKeyboardState = Keyboard.GetState();
protected KeyboardState keyboardState = Keyboard.GetState();

Comment them out:
//protected KeyboardState previousKeyboardState = Keyboard.GetState();
//protected KeyboardState keyboardState = Keyboard.GetState();

Now compile the code and smile as the errors appear. I got 32. We'll fix them in just a second, but while we're here in the variable declarations, let's declare a KeyboardManager variable right below the newly commented code:
protected KeyboardManager keyboardManager = new KeyboardManager();

Okay, let's go down to where the errors are. They're all in Update(). The first two lines in Update() set the values for keyboardState and previousKeyboardState. We'll replace these with the keyboardManager.Update() method.
Comment out these lines:
previousKeyboardState = keyboardState;
keyboardState = Keyboard.GetState();

and add this line in their place:
keyboardManager.Update(gameTime);

This line of code updates the keyboard manager and should be called before any other keyboard manager calls in Update().
Next thing we want to do is comment out all lines of code that have keyboardState in them. This includes everything below:
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
    keyboardState.IsKeyDown(Keys.Escape) || keyboardState.IsKeyDown(Keys.Q) || keyboardState.IsKeyDown(Keys.F10) || keyboardState.IsKeyDown(Keys.F12))
    Exit();
 
// If the player presses up, give it thrust
if (keyboardState.IsKeyDown(Keys.Up) || keyboardState.IsKeyDown(Keys.W) || keyboardState.IsKeyDown(Keys.NumPad8))
{
    spaceship.Thrust();
    UpdateEngineSound(true); // Make the engine sound turn on
}
else
{
    UpdateEngineSound(false); // Make the engine sound turn off
}
 
// If the player presses left/right, turn that way
if (keyboardState.IsKeyDown(Keys.Left) || keyboardState.IsKeyDown(Keys.A) || keyboardState.IsKeyDown(Keys.NumPad4))
{
    spaceship.TurnLeft();
}
if (keyboardState.IsKeyDown(Keys.Right) || keyboardState.IsKeyDown(Keys.D) || keyboardState.IsKeyDown(Keys.NumPad6))
{
    spaceship.TurnRight();
}
 
// If the player presses space, fire the gun
if (keyboardState.IsKeyDown(Keys.Space) && previousKeyboardState.IsKeyUp(Keys.Space) || 
    keyboardState.IsKeyDown(Keys.LeftControl) && previousKeyboardState.IsKeyUp(Keys.LeftControl) ||
    keyboardState.IsKeyDown(Keys.RightControl) && previousKeyboardState.IsKeyUp(Keys.RightControl) || 
    keyboardState.IsKeyDown(Keys.NumPad0) && previousKeyboardState.IsKeyUp(Keys.NumPad0) ||
    keyboardState.IsKeyDown(Keys.F) && previousKeyboardState.IsKeyUp(Keys.F))
{
    spaceship.Fire();
    soundLaser.Play();
}
 
// If the player presses enter, teleport somewhere
if (keyboardState.IsKeyDown(Keys.Enter) && previousKeyboardState.IsKeyUp(Keys.Enter) ||
    keyboardState.IsKeyDown(Keys.LeftShift) && previousKeyboardState.IsKeyUp(Keys.LeftShift) ||
    keyboardState.IsKeyDown(Keys.RightShift) && previousKeyboardState.IsKeyUp(Keys.RightShift))
{
    spaceship.Hyperspace();
    soundHyperspaceActivation.Play(); // Play the hyperspace sound
}

Now it should look like this:
//// Allows the game to exit
//if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
//    keyboardState.IsKeyDown(Keys.Escape) || keyboardState.IsKeyDown(Keys.Q) || keyboardState.IsKeyDown(Keys.F10) || keyboardState.IsKeyDown(Keys.F12))
//    Exit();
 
//// If the player presses up, give it thrust
//if (keyboardState.IsKeyDown(Keys.Up) || keyboardState.IsKeyDown(Keys.W) || keyboardState.IsKeyDown(Keys.NumPad8))
//{
//    spaceship.Thrust();
//    UpdateEngineSound(true); // Make the engine sound turn on
//}
//else
//{
//    UpdateEngineSound(false); // Make the engine sound turn off
//}
 
//// If the player presses left/right, turn that way
//if (keyboardState.IsKeyDown(Keys.Left) || keyboardState.IsKeyDown(Keys.A) || keyboardState.IsKeyDown(Keys.NumPad4))
//{
//    spaceship.TurnLeft();
//}
//if (keyboardState.IsKeyDown(Keys.Right) || keyboardState.IsKeyDown(Keys.D) || keyboardState.IsKeyDown(Keys.NumPad6))
//{
//    spaceship.TurnRight();
//}
 
//// If the player presses space, fire the gun
//if (keyboardState.IsKeyDown(Keys.Space) && previousKeyboardState.IsKeyUp(Keys.Space) || 
//    keyboardState.IsKeyDown(Keys.LeftControl) && previousKeyboardState.IsKeyUp(Keys.LeftControl) ||
//    keyboardState.IsKeyDown(Keys.RightControl) && previousKeyboardState.IsKeyUp(Keys.RightControl) || 
//    keyboardState.IsKeyDown(Keys.NumPad0) && previousKeyboardState.IsKeyUp(Keys.NumPad0) ||
//    keyboardState.IsKeyDown(Keys.F) && previousKeyboardState.IsKeyUp(Keys.F))
//{
//    spaceship.Fire();
//    soundLaser.Play();
//}
 
//// If the player presses enter, teleport somewhere
//if (keyboardState.IsKeyDown(Keys.Enter) && previousKeyboardState.IsKeyUp(Keys.Enter) ||
//    keyboardState.IsKeyDown(Keys.LeftShift) && previousKeyboardState.IsKeyUp(Keys.LeftShift) ||
//    keyboardState.IsKeyDown(Keys.RightShift) && previousKeyboardState.IsKeyUp(Keys.RightShift))
//{
//    spaceship.Hyperspace();
//    soundHyperspaceActivation.Play(); // Play the hyperspace sound
//}

The program should compile now, so you can run it. However, you won't be able to move or fire anymore. Little by little we'll uncomment this code and fix it. For now, though, let's go to the bottom of the LoadContent() method. There is a comment at the bottom that says:
// Register keys in KeyboardManager

We're going to register the action "exit" with the escape key. Right below the comment, put this line of code:
keyboardManager.RegisterCommand("exit", Keys.Escape, KeyboardCommandType.Pressed);

This method, RegisterCommand, associates the action and the key. The first parameter is the command which we are calling "exit". The second parameter is the key with which we are associating the command. The third command is the type of command we want it to be. In this case, it is Pressed. This means that any time the player presses the escape key and it occurs only once (not every time the Update() method is called). The other options for KeyboardCommandType are Hold and CoolDown, which will be explained later.
Once we register the command, we need to check if the command should be executed. This is done back in Update().
Uncomment these lines of code:
//// Allows the game to exit
//if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
//    keyboardState.IsKeyDown(Keys.Escape) || keyboardState.IsKeyDown(Keys.Q) || keyboardState.IsKeyDown(Keys.F10) || keyboardState.IsKeyDown(Keys.F12))
//    Exit();

and remove everything on the keyboardState line except the last ")". Now those lines should look like this:
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
    )
    Exit();

In keyboardState's place, put the following line:
keyboardManager.IsExecuteCommand("exit")

So the entire thing should look like this:
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
    keyboardManager.IsExecuteCommand("exit"))
    Exit();

Now if you run it you should be able to press the escape key and it will exit. The KeyboardManager doesn't really check to see if a key is down or up, it checks to see if a command should be executed. In this case, it is checking if the "exit" command should be executed. If the player presses the escape key, which is associated with "exit", KeyboardManager will return true - "exit" should be executed.
Next let's do the "thrust" command that causes the ship to move forward. At the bottom of LoadContent() right below the keyboardManager.RegisterCommand() method, put this line:
keyboardManager.RegisterCommand("thrust", Keys.Up, KeyboardCommandType.Hold);

Similar to the other RegisterCommand method call above it, this one also associates an action with a keyboard key, but this time it is the "thrust" action with the Up key. Also notice that we aren't using KeyboardCommandType.Pressed anymore and are instead using KeyboardCommandType.Hold. Hold means that any time the key is held down to perform the action.
Go back down to the Update() method and uncomment the following lines:
//// If the player presses up, give it thrust
//if (keyboardState.IsKeyDown(Keys.Up) || keyboardState.IsKeyDown(Keys.W) || keyboardState.IsKeyDown(Keys.NumPad8))
//{
//    spaceship.Thrust();
//    UpdateEngineSound(true); // Make the engine sound turn on
//}
//else
//{
//    UpdateEngineSound(false); // Make the engine sound turn off
//}

and replace everything in the if statement with this:
keyboardManager.IsExecuteCommand("thrust")

Notice that this command is exactly the same as the "exit" command even though one is checking if the key is pressed and the other held down.
The thrust code should now look like this:
// If the player presses up, give it thrust
if (keyboardManager.IsExecuteCommand("thrust"))
{
    spaceship.Thrust();
    UpdateEngineSound(true); // Make the engine sound turn on
}
else
{
    UpdateEngineSound(false); // Make the engine sound turn off
}

We'll now register the code for turning in exactly the same way as thrust:
Add these below the rest of the register code:
keyboardManager.RegisterCommand("turnleft", Keys.Left, KeyboardCommandType.Hold);
keyboardManager.RegisterCommand("turnright", Keys.Right, KeyboardCommandType.Hold);

Uncomment these lines of code:
//// If the player presses left/right, turn that way
//if (keyboardState.IsKeyDown(Keys.Left) || keyboardState.IsKeyDown(Keys.A) || keyboardState.IsKeyDown(Keys.NumPad4))
//{
//    spaceship.TurnLeft();
//}
//if (keyboardState.IsKeyDown(Keys.Right) || keyboardState.IsKeyDown(Keys.D) || keyboardState.IsKeyDown(Keys.NumPad6))
//{
//    spaceship.TurnRight();
//}

Replace everything in the if statements with this for the left:
keyboardManager.IsExecuteCommand("turnleft")

and this for the right:
keyboardManager.IsExecuteCommand("turnright")

You should be able to move and turn. Let's get to firing now.
Start by adding this to the register command section:
keyboardManager.RegisterCommand("fire", Keys.Space, KeyboardCommandType.CoolDown, 0.25);

This is the final KeyboardCommandType enumeration. CoolDown means that the action will not execute again until a certain amount of time has elapsed. In our case, the last parameter was 0.25 so the fire command will only execute every 0.25 seconds. If you don't specify the last parameter on KeyboardCommandType.CoolDown, it will default to 0.0 seconds and will be treated just like KeyboardCommandType.Hold.
Just like all the other ones, uncomment the fire lines of code in Update:
//// If the player presses space, fire the gun
//if (keyboardState.IsKeyDown(Keys.Space) && previousKeyboardState.IsKeyUp(Keys.Space) || 
//    keyboardState.IsKeyDown(Keys.LeftControl) && previousKeyboardState.IsKeyUp(Keys.LeftControl) ||
//    keyboardState.IsKeyDown(Keys.RightControl) && previousKeyboardState.IsKeyUp(Keys.RightControl) || 
//    keyboardState.IsKeyDown(Keys.NumPad0) && previousKeyboardState.IsKeyUp(Keys.NumPad0) ||
//    keyboardState.IsKeyDown(Keys.F) && previousKeyboardState.IsKeyUp(Keys.F))
//{
//    spaceship.Fire();
//    soundLaser.Play();
//}

remove everything in the if statement and replace it with this:
keyboardManager.IsExecuteCommand("fire")

So the final fire code block should look like this:
// If the player presses space, fire the gun
if (keyboardManager.IsExecuteCommand("fire"))
{
    spaceship.Fire();
    soundLaser.Play();
}

Now if you play the game you'll fire every 0.25 seconds as long as you're holding down the spacebar. It's nice that it does this automatically for us and we don't have to pound on the spacebar anymore just to shoot.
Last thing we will want to add is the teleport ability. This is just like fire except instead of every 0.25 seconds we should have it teleport every 1.0 seconds.
Add this line of code to the register command code:
keyboardManager.RegisterCommand("teleport", Keys.Enter, KeyboardCommandType.CoolDown, 1.0);

Uncomment these lines in Update():
//// If the player presses enter, teleport somewhere
//if (keyboardState.IsKeyDown(Keys.Enter) && previousKeyboardState.IsKeyUp(Keys.Enter) ||
//    keyboardState.IsKeyDown(Keys.LeftShift) && previousKeyboardState.IsKeyUp(Keys.LeftShift) ||
//    keyboardState.IsKeyDown(Keys.RightShift) && previousKeyboardState.IsKeyUp(Keys.RightShift))
//{
//    spaceship.Hyperspace();
//    soundHyperspaceActivation.Play(); // Play the hyperspace sound
//}

Replace everything in the if statement with this:
keyboardManager.IsExecuteCommand("teleport")

and the final code block for teleport is this:
// If the player presses enter, teleport somewhere
if (keyboardManager.IsExecuteCommand("teleport"))
{
    spaceship.Hyperspace();
    soundHyperspaceActivation.Play(); // Play the hyperspace sound
}

There is one more thing to do. Imagine we were working with a menu. The input is slightly more complicated than either pressing the up/down buttons and having the options change. We want it to be when we press up/down it will move every time. However, we also want it so that if the player just holds down the up/down buttons it will scroll a few times a second. Fortunately, this is easily done with KeyboardManager. All we have to do is register the action and key again with a different KeyboardCommandType. Let's do this with teleport.
Add the following to the register command code:
keyboardManager.RegisterCommand("teleport", Keys.Enter, KeyboardCommandType.Pressed);

Now when you play the game and teleport, every time you press enter you will teleport. Or if you just hold down enter you will still teleport every second.

That's everything for this tutorial. Week1Finish.zip contains the finished code with all of the above changes. Feel free to use KeyboardManager.cs in other projects. If you do, though, remember to change the namespace (currently Week1) to your project's namespace.
I'll include KeyboardManager.cs here so you can copy it if you need it. And if you've got any questions or comments, feel free to email me and let me know.

using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
 
namespace Week1
{
    /// <summary>
    /// Hold - the command is true whenever the key is held down
    /// Pressed - the command is true whenever the key is pressed
    /// CoolDown - same as Hold
    /// </summary>
    public enum KeyboardCommandType
    {
        Hold,
        Pressed,
        CoolDown,
    }
 
    class KeyboardCommand
    {
        public KeyboardCommand(string command, Keys key, KeyboardCommandType keyboardCommandType, double cooldownTime)
        {
            this.command = command;
            this.key = key;
            this.keyboardCommandType = keyboardCommandType;
            this.cooldownTime = cooldownTime;
        }
        public string command;
        public Keys key;
        public KeyboardCommandType keyboardCommandType;
        public double cooldownTime;
        public double lastExecuted;
    }
 
    public class KeyboardManager
    {
        #region private members
 
        private GameTime gameTime;
        private KeyboardState keyboardState = Keyboard.GetState();
        private KeyboardState previousKeyboardState = Keyboard.GetState();
 
        private readonly Dictionary<string, List<KeyboardCommand>> CommandMap = new Dictionary<string, List<KeyboardCommand>>();
 
        #endregion
 
        #region Public methods
 
        /// <summary>
        /// Call this method once per update before calling any other methods in Update().
        /// </summary>
        /// <param name="gameTime">gameTime argument passed to Game1.Update().</param>
        public void Update(GameTime gameTime)
        {
            this.gameTime = gameTime;
            previousKeyboardState = keyboardState;
            keyboardState = Keyboard.GetState();
        }
 
        /// <summary>
        /// Associates a key with a command with the keyboard manager. Assumes no cooldown time.
        /// </summary>
        /// <param name="command">Commands are things like "menu_up", "menu_select", "fire_main_gun".</param>
        /// <param name="key">The keyboard key that to be associated with the specified command.</param>
        /// <param name="keyboardCommandType">The type of command it will be</param>
        public void RegisterCommand(string command, Keys key, KeyboardCommandType keyboardCommandType)
        {
            RegisterCommand(command, key, keyboardCommandType, 0.0);
        }
 
        /// <summary>
        /// Associates a key with a command with the keyboard manager.
        /// </summary>
        /// <param name="command">Commands are things like "menu_up", "menu_select", "fire_main_gun".</param>
        /// <param name="key">The keyboard key that to be associated with the specified command.</param>
        /// <param name="keyboardCommandType">The type of command it will be</param>
        /// <param name="coolDown">Only used for KeyboardCommandType.CoolDown. The amount of time (in seconds) that must occur before the command can be called again.</param>
        public void RegisterCommand(string command, Keys key, KeyboardCommandType keyboardCommandType, double coolDown)
        {
            // Check if the command already exists
            // If it does, just add the key to the list
            if (CommandMap.ContainsKey(command))
            {
                CommandMap[command].Add(new KeyboardCommand(command, key, keyboardCommandType, coolDown));
            }
            else
            {
                List<KeyboardCommand> keyList = new List<KeyboardCommand>();
                keyList.Add(new KeyboardCommand(command, key, keyboardCommandType, coolDown));
                CommandMap.Add(command, keyList);
            }
        }
 
        /// <summary>
        /// Returns true if the command should execute right now.
        /// </summary>
        /// <returns>True if the command should execute, false otherwise.</returns>
        public bool IsExecuteCommand(string command)
        {
            // Check to see if the key is being pressed
            foreach(KeyboardCommand c in CommandMap[command])
            {
                switch(c.keyboardCommandType)
                {
                    // If the command type is hold, we only need to make sure the key is down
                    case KeyboardCommandType.Hold:
                        if(keyboardState.IsKeyDown(c.key))
                        {
                            c.lastExecuted = gameTime.TotalGameTime.TotalSeconds;
                            return true;
                        }
                        break;
                    // If the command type is pressed, the key must be down and in the previous state
                    case KeyboardCommandType.Pressed:
                        if (keyboardState.IsKeyDown(c.key) && previousKeyboardState.IsKeyUp(c.key))
                        {
                            c.lastExecuted = gameTime.TotalGameTime.TotalSeconds;
                            return true;
                        }
                        break;
                    // If the command has a cooldown, it's the same as hold except it must also make sure the cooldown time has expired
                    case KeyboardCommandType.CoolDown:
                        if (keyboardState.IsKeyDown(c.key))
                        {
                            // Check for cooldown
                            if (c.lastExecuted + c.cooldownTime 
                                < gameTime.TotalGameTime.TotalSeconds)
                            {
                                c.lastExecuted = gameTime.TotalGameTime.TotalSeconds;
                                return true;
                            }
                        }
                        break;
                }
            }
            return false;
        }
 
        #endregion
    }
}

back to SIGXNA

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License