CHAPTER 5
Until voice-controlled games become the standard, game developers will have to give players a way to control their games. Fortunately, MonoGame makes it easy to hook input devices into your game. In this chapter, we’ll build an input system that recognizes the player using these devices that you can reuse in all of your games. We’ll take a look at the three devices that are typically used in PC and console games. Mobile devices have additional functionality in the form of touch that is the main source of input, but we’re only concentrating on the first two platforms. If you want to port your games to mobile platforms, you’ll have to add touch capability into the input system we’ll develop here.
The gamepad is a combination of mouse and keyboard for consoles, since most gamers don’t want nor have these devices in their living room or other rooms in which consoles are normally used. Gamepads are also much smaller and easier to store when not in use. They don’t have quite as much flexibility as a mouse and keyboard due to the limited number of types of input, so you may have to get creative with how you use them in your game. One method (as seen in the RPG genre) is using on-screen UI elements such as dialogs, menus, and toolbars that the player can bring up or toggle to from controlling their character.
Input devices can have two kinds of controls: digital and analog. Digital controls can be in only one of two states: on or off. Keys on a keyboard are examples of digital controls, as are the buttons on the mouse, and the buttons and DPad on the Xbox 360 controller. Analog controls can have a value within a specific range. The sticks and triggers on the Xbox 360 controller and movements of the mouse return values within a range. The Xbox 360 controller sticks return a floating-point value between −1.0 and 1.0, and the triggers return a value between 0.0 and 1.0. For the mouse, mouse cursor values are returned in pixels.
Each input device has specific advantages and disadvantages. The following table shows a brief comparison:
Input Device | Digital Buttons | Analog Controls | Vibration Effects | Supported on Windows | Supported on Xbox | # Allowed on System |
|---|---|---|---|---|---|---|
Xbox Controller | 14 | 4 | Yes | Yes | Yes | 4 |
Keyboard | > 100 | 0 | No | Yes | Yes | 1 |
Mouse | 5 | 3 | No | Yes | No | 1 |
Table 3 – Input Device Platform Comparison
Since you can’t assume that a player on a PC is using a specific type of gamepad (or even an actual gamepad), the GamePad class provides two methods called GetCapabilities for getting the controls on the device that is being used. One method takes an int parameter, the other a PlayerIndex parameter.
The methods return a GamePadCapabilities structure with the following properties:
Name | Description |
|---|---|
DisplayName | Gets the gamepad display name. |
GamePadType | Gets the type of the controller |
HasAButton | Gets a value indicating whether the controller has the button A |
HasBButton | Gets a value indicating whether the controller has the button B |
HasBackButton | Gets a value indicating whether the controller has the button Back |
HasBigButton | Gets a value indicating whether the controller has the guide button |
HasDPadDownButton | Gets a value indicating whether the controller has the directional pad down button |
HasDPadLeftButton | Gets a value indicating whether the controller has the directional pad left button |
HasDPadRightButton | Gets a value indicating whether the controller has the directional pad right button |
HasDPadUpButton | Gets a value indicating whether the controller has the directional pad up button |
HasLeftShoulderButton | Gets a value indicating whether the controller has the left shoulder button |
HasLeftStickButton | Gets a value indicating whether the controller has the left stick button |
HasLeftTrigger | Gets a value indicating whether the controller has the left trigger button |
HasLeftVibrationMotor | Gets a value indicating whether the controller has the left vibration motor |
HasLeftXThumbStick | Gets a value indicating whether the controller has X axis for the left stick (thumbstick) button |
HasLeftYThumbStick | Gets a value indicating whether the controller has Y axis for the left stick (thumbstick) button |
HasRightShoulderButton | Gets a value indicating whether the controller has the right shoulder button |
HasRightStickButton | Gets a value indicating whether the controller has the right stick button |
HasRightTrigger | Gets a value indicating whether the controller has the right trigger button |
HasRightVibrationMotor | Gets a value indicating whether the controller has the right vibration motor |
HasRightXThumbStick | Gets a value indicating whether the controller has X axis for the right stick (thumbstick) button |
HasRightYThumbStick | Gets a value indicating whether the controller has Y axis for the right stick (thumbstick) button |
HasStartButton | Gets a value indicating whether the controller has the button Start |
HasVoiceSupport | Gets a value indicating whether the controller has a microphone |
HasXButton | Gets a value indicating whether the controller has the button X |
HasYButton | Gets a value indicating whether the controller has the button Y |
Identifier | Gets the unique identifier of the gamepad. |
IsConnected | Gets a value indicating if the controller is connected |
Table 4 – GamePadCapabilities Structure Properties
If you’re going to support any type of controller, you’ll want to check the GamePadType property to make sure whatever controls you want to use are supported on the controller the player is using. You’ll also want to check this if your game is in a genre that needs a specific type of controller, such as a music or rhythm game, or an aircraft simulator or flight game where the player could use a flight stick. The possible values for the GamePadType are:
Name | Description |
|---|---|
AlternateGuitar | GamePad is an alternate guitar |
ArcadeStick | GamePad is an arcade stick |
BigButtonPad | GamePad is a big button pad |
DancePad | GamePad is a dance pad |
DrumKit | GamePad is a drum kit |
FlightStick | GamePad is a flight stick |
GamePad | GamePad is the XBOX controller |
Guitar | GamePad is a guitar |
Unknown | Unknown |
Wheel | GamePad is a wheel |
Table 5 – GamePadType Fields
The BigButtonPad is a specific type of controller Microsoft introduced with the “Scene It?” games that is useful for quiz-type games. It has a large button at the top that acts as a buzzer and four smaller buttons for answering multiple-choice questions.
If your game uses the vibration motors you’ll use the two SetVibration methods to control them. The methods take three parameters, with the first being either a PlayerIndex or int, and the other two being floats.
There are four GetState methods, although you’ll probably only use the two that take either a PlayerIndex or int parameter. The other two take a GamePadDeadZone parameter that is useful for analog stick controllers.
Every frame, the state of the available input devices is updated and made available through various structures. The first we’ll look at is the structure that contains data for a gamepad. The GamePadState structure gives us the state of the following controls and methods for getting some of them:
Properties |
Buttons |
DPad |
IsConnected |
ThumbSticks |
Triggers |
Methods |
IsButtonDown |
IsButtonUp |
Table 6 – GamePadState Structure Properties and Methods
The structure offers a few more properties and methods than this, but these are the ones that will be used most often.
The properties representing the various controls on the gamepad are themselves structures that contains properties for the various options for that property. Buttons contains all basic controls: A/B/X/Y buttons, Back/Start buttons, and Shoulders/Sticks. Each property in that structure is an enum with a value of Pressed or Released.
DPad has Left, Right, Up, and Down properties that are either Pressed or Released.
Triggers and ThumbSticks use a float and Vector2 data type for the Left and Right controls, respectively, since they do not have to be completely pressed or released. These provide detail that the Button properties do not have. If you’re using either in such a way that you only need to know if they’re pressed or not, use the Buttons properties. This could be for something like navigating UI, for example. You don’t need to know how far the control is pressed, just whether or not it is. If it is, you might move to the next UI element that can be selected. If you’re using the thumbsticks to control a character, you will need to know in what direction and possibly how much it’s being moved.
The IsButtonUp and IsButtonDown methods take a Buttons enum parameter and return a Boolean to indicate if the button or buttons (the parameter can be a bitwise OR to check multiple buttons at once) is pressed or released. This is useful if you have a game that uses combinations of buttons to perform actions, and is easier than checking each button individually.
The Keyboard class contains just two methods, which are used for getting the state of the keyboard(s). The first takes no parameter and returns the state of the current keyboard. The second takes a PlayerIndex parameter and returns the state of the keyboard for a specific player. This method is obsolete, however, so should be avoided.
The KeyboardState structure has three properties, which you probably won’t use a lot, and three methods, which will probably be what you’ll rely on most often.
Properties | Description |
CapsLock | Returns the current state of the CapsLock key |
Item | Returns the current state of a specific key |
NumLock | Returns the current state of the NumLock key |
Methods | Description |
GetPressedKeys | Returns an array of the keys that are currently pressed |
IsKeyDown | Returns a Boolean that indicates if the specified key is pressed |
IsKeyUp | Returns a Boolean that indicates if the specified key is not pressed |
Table 7 – KeyboardState Structure Properties and Methods
The GetPressedKeys method is useful if you need to check a lot of keys at the same time without calling the IsKey methods multiple times. This is handy for games that use key combinations. If you only want to check whether a key is pressed or not, the IsKey methods are what you’ll want to use.
Like the Keyboard class, the Mouse class is mainly used for getting the state of the mouse. There are two other methods, SetCursor and SetPosition, that you might find useful in certain situations, however.
Instead of drawing your own cursor, you can call the SetCursor method to use the Windows cursor and change it to one of the supplied cursors:
Name | Description |
|---|---|
Arrow | Gets the default arrow cursor |
Crosshair | Gets the crosshair (“+”) cursor |
Hand | Gets the hand cursor |
Handle | n/a |
IBeam | Gets the cursor that appears when the mouse is over text editing regions |
No | Gets the cursor that points that something is invalid, usually a cross |
SizeAll | Gets the cursor which points in all directions |
SizeNESW | Gets the northeast/southwest (“/”) cursor |
SizeNS | Gets the vertical north/south (“|”) cursor |
SizeNWSE | Gets the northwest/southeast (“\”) cursor |
SizeWE | Gets the horizontal (“-“) cursor |
Wait | Gets the waiting cursor that appears while the application/system is busy |
WaitArrow | Gets the cross between Arrow and Wait cursors |
Table 8 – KeyboardState Structure Properties and Methods
You would have to ensure the cursor is visible by setting the the Game.IsMouseVisible member to true.
The Crosshair cursor would be useful as an aiming reticle, the Hand for indicating that something can be picked up, etc.
The SetPosition method takes two int parameters for the x and y position of the mouse cursor. This could be useful if you’re using the Windows cursor and want to lock it into a certain area or point.
The MouseState structure contains the following properties:
Name | Description |
|---|---|
HorizontalScrollWheelValue | Returns the cumulative horizontal scroll wheel value since the game start |
LeftButton | n/a |
MiddleButton | n/a |
Position | n/a |
RightButton | n/a |
ScrollWheelValue | Returns cumulative scroll wheel value since the game start |
X | n/a |
XButton1 | n/a |
XButton2 | n/a |
Y | n/a |
Table 9 – MouseState Structure Properties
Most of the properties are fairly obvious. I’ve noted the two that could be misleading. The scroll wheel properties are tracked since the start of the game, not since the last frame. If you want frame-based values, you’ll have to track them yourself.
There are quite a few modifications necessary to our game to get a nice input system up and running. First, add a class to the ScreenManager folder called InputState, and add the following code to it:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; public class InputState { public KeyboardState CurrentKeyboardState; public GamePadState CurrentGamePadState; public KeyboardState LastKeyboardState; public GamePadState LastGamePadState; public bool MenuUp { get { return IsNewKeyPress(Keys.Up) || (CurrentGamePadState.DPad.Up == ButtonState.Pressed && LastGamePadState.DPad.Up == ButtonState.Released) || (CurrentGamePadState.ThumbSticks.Left.Y > 0 && LastGamePadState.ThumbSticks.Left.Y <= 0); } } public bool MenuDown { get { return IsNewKeyPress(Keys.Down) || (CurrentGamePadState.DPad.Down == ButtonState.Pressed && LastGamePadState.DPad.Down == ButtonState.Released) || (CurrentGamePadState.ThumbSticks.Left.Y < 0 && LastGamePadState.ThumbSticks.Left.Y >= 0); } } public bool MenuSelect { get { return IsNewKeyPress(Keys.Space) || IsNewKeyPress(Keys.Enter) || (CurrentGamePadState.Buttons.A == ButtonState.Pressed && LastGamePadState.Buttons.A == ButtonState.Released) || (CurrentGamePadState.Buttons.Start == ButtonState.Pressed && LastGamePadState.Buttons.Start == ButtonState.Released); } } public bool MenuCancel { get { return IsNewKeyPress(Keys.Escape) || (CurrentGamePadState.Buttons.B == ButtonState.Pressed && LastGamePadState.Buttons.B == ButtonState.Released) || (CurrentGamePadState.Buttons.Back == ButtonState.Pressed && LastGamePadState.Buttons.Back == ButtonState.Released); } } public bool PauseGame { get { return IsNewKeyPress(Keys.Escape) || (CurrentGamePadState.Buttons.Back == ButtonState.Pressed && LastGamePadState.Buttons.Back == ButtonState.Released) || (CurrentGamePadState.Buttons.Start == ButtonState.Pressed && LastGamePadState.Buttons.Start == ButtonState.Released); } } public void Update() { LastKeyboardState = CurrentKeyboardState; LastGamePadState = CurrentGamePadState; CurrentKeyboardState = Keyboard.GetState(); CurrentGamePadState = GamePad.GetState(PlayerIndex.One); } public bool IsNewKeyPress(Keys key) { return (CurrentKeyboardState.IsKeyDown(key) && LastKeyboardState.IsKeyUp(key)); } public bool IsKeyDown(Keys key) { return (CurrentKeyboardState.IsKeyDown(key)); } public bool IsNewKeyUp(Keys key) { return (CurrentKeyboardState.IsKeyUp(key) && LastKeyboardState.IsKeyDown(key)); } } |
Code Listing 39 – InputState class
The class holds the state of the keyboard and gamepad for the current frame and the previous frame. This is necessary because examining only the current frame would give false results. If the player holds down the control to fire, for example, even for only a fraction of a second, the game would process that as multiple fire actions if the code only examines the current input state. We need to detect the first frame in which the player initiates a fire action, and this can only be done by comparing the previous frame’s data with the current frame.
The Update method copies what it has as the current frame input data to the previous frame’s input data before obtaining the actual current frame input data via the GetState methods for the keyboard and gamepad.
The Menu… and Pause members check all the various controls that can be used to navigate a menu and return the appropriate value, depending on whether or not the control for that action was used.
The Is…members check the current keyboard state or the current and previous keyboard state for the key passed and return the appropriate value if the key was pressed, up, or down that frame.
As every screen will use this class, we’ll add a virtual method to the GameScreen class that each instance will override:
public virtual void HandleInput(InputState input, GameTime gameTime) { } |
Code Listing 40 – GameScreen HandleInput Method
This class is only used for the non-gameplay input. We’ll need something a little more robust and flexible for controlling our character in the actual game.
Our input system will use an interface and class to create a system that will map a specific control to a specific action. We’ll have seven different possible actions the player can take:
The interface looks fairly simple, but allows a lot of flexibility. Create a file call IInputSystem in the root project folder and add the following code to it:
using System.Collections.Generic; interface IInputSystem { List<MappedAction> MappedActions { get; set; } void SetActionFunction(string name, ActionDelegate function); void Enable(); } |
Code Listing 41 – IInputSystem Interface
The MappedAction class is what will allow us to associate a control with an action.
The SetActionFunction is basically what its name says—it allows us to add a function for an action that’s associated with a user-friendly name. The implementation of this method will be done in the InputSystem class we’ll add soon that implements this interface.
The Enable method lets us disable the input system until we get into the actual game. We set up the input system when the game starts and allow the user to configure the controls for actions, but we don’t want the system to actually run until the gameplay starts, since we have a different system that controls the menu system.
It’s time to add the MappedAction class that this interface references. Add this class file to the root folder, and fill it with the following code:
class MappedAction { private string _name; private Control _control; private ActionType _action; private ActionDelegate _function; public MappedAction(string name, Control control, ActionType action, ActionDelegate function) { _name = name; _control = control; _action = action; _function = function; } public string Name { get { return _name; } set { _name = value; } } public Control ActionControl { get { return _control; } set { _control = value; } } public ActionType Action { get { return _action; } set { _action = value; } } public ActionDelegate Function { get { return (ActionDelegate) _function; } set { _function = value; } } } |
Code Listing 42 – MappedAction Class
The Name member allows us to show the user actions as correct English. Since enums cannot have spaces between the different words and might not be named exactly as what you would want to show the user, we have a member that can be set to exactly what we want to show the user.
The Control and ActionType members are enums. We’ll define them now. Add a file called InputSystem to the root folder and add the enums for these two members:
public enum ActionType { Move, MoveForward, MoveBackward, MoveLeft, MoveRight, Rotate, Fire } public enum Control { AButton, BButton, XButton, YButton, StartButton, BackButton, LeftTrigger, RightTrigger, LeftShoulder, RightShoulder, LeftStick, RightStick, DPadUp, DPadDown, DPadLeft, DPadRight, None, Back, Tab, Enter, CapsLock, Escape, Space, PageUp, PageDown, End, Home, Left, Up, Right, Down, Select, Print, Execute, PrintScreen, Insert, Delete, Help, D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, LeftWindows, RightWindows, Apps, Sleep, NumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9, Multiply, Add, Separator, Subtract, Decimal, Divide, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, NumLock, Scroll, LeftShift, RightShift, LeftControl, RightControl, LeftAlt, RightAlt, BrowserBack, BrowserForward, BrowserRefresh, BrowserStop, BrowserSearch, BrowserFavorites, BrowserHome, VolumeMute, VolumeDown, VolumeUp, MediaNextTrack, MediaPreviousTrack, MediaStop, MediaPlayPause, LaunchMail, SelectMedia, LaunchApplication1, LaunchApplication2, OemSemicolon, OemPlus, OemComma, OemMinus, OemPeriod, OemQuestion, OemTilde, OemOpenBrackets, OemPipe, OemCloseBrackets, OemQuotes, Oem8, OemBackslash, ProcessKey, Attn, Crsel, Exsel, EraseEof, Play, Zoom, Pa1, OemClear, LeftMouseButton, MiddleMouseButton, RightMouseButton, MouseButton1, MouseButton2 } |
Code Listing 43 – Input System Enums
The Control enum has all possible gamepad, keyboard, and mouse input controls. Many of them you wouldn’t ever consider using for controls for an action, but they’re including for future expansion for any other functionality you might want to add.
The ActionDelegate function is defined as follows:
public delegate void ActionDelegate(object value, GameTime gameTime); |
Code Listing 44 – ActionDelegate Defintion
If you’re not familiar with delegates, we’re using them here to define the parameters that are passed for events for each type of action the user can have the character perform. These events will be in the InputSystem class that we’ll define now. Add this to the InputSystem file:
|
class InputSystem : GameComponent, IInputSystem { public event ActionDelegate Move; public event ActionDelegate MoveForward; public event ActionDelegate MoveBackward; public event ActionDelegate MoveLeft; public event ActionDelegate MoveRight; public event ActionDelegate Stop; public event ActionDelegate Rotate; public event ActionDelegate Fire; private GamePadState _lastGamePadState; private KeyboardState _lastKeyboardState; private GamePadState _curGamePadState; private KeyboardState _curKeyboardState; private MouseState _curMouseState; private MouseState _lastMouseState; private List<MappedAction> _mappedActions; public List<MappedAction> MappedActions { get { return _mappedActions; } set { _mappedActions = value; } } public InputSystem(Game game) : base(game) { _mappedActions = new List<MappedAction>(); } public override void Initialize() { } public void Enable() { this.Enabled = true; }
public void AddAction(string name, Control control, ActionType type, ActionDelegate function) { MappedAction action = new MappedAction(name, control, type, function); _mappedActions.Add(action); } public void SetActionControl(string name, Control control) { foreach (MappedAction action in _mappedActions) { if (action.Name == name) { action.ActionControl = control; break; } } } public void SetActionFunction(string name, ActionDelegate function) { if (name == "Stop") this.Stop += function; foreach (MappedAction action in _mappedActions) { if (action.Name == name) { action.Function = function; break; } } } public override void Update(GameTime gameTime) { _curGamePadState = GamePad.GetState(PlayerIndex.One); _curKeyboardState = Keyboard.GetState(); _curMouseState = Mouse.GetState(); int dir = -1; bool moving = false; //we only need to look at the controls mapped to actions if (Enabled) { foreach (MappedAction action in _mappedActions) { switch (action.ActionControl) { case Control.AButton: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.A, gameTime); } else { if (_curGamePadState.Buttons.A == ButtonState.Pressed && _lastGamePadState.Buttons.A == ButtonState.Released) action.Function(null, gameTime); } break; case Control.BButton: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.B, gameTime); } else { if (_curGamePadState.Buttons.B == ButtonState.Pressed && _lastGamePadState.Buttons.B == ButtonState.Released) action.Function(null, gameTime); } break; case Control.XButton: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.X, gameTime); } else { if (_curGamePadState.Buttons.X == ButtonState.Pressed && _lastGamePadState.Buttons.X == ButtonState.Released) action.Function(null, gameTime); } break; case Control.YButton: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.Y, gameTime); } else { if (_curGamePadState.Buttons.Y == ButtonState.Pressed && _lastGamePadState.Buttons.Y == ButtonState.Released) action.Function(null, gameTime); } break; case Control.StartButton: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.Start, gameTime); } else { if (_curGamePadState.Buttons.Start == ButtonState.Pressed && _lastGamePadState.Buttons.Start == ButtonState.Released) action.Function(null, gameTime); } break; case Control.BackButton: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.Back, gameTime); } else { if (_curGamePadState.Buttons.Back == ButtonState.Pressed && _lastGamePadState.Buttons.Back == ButtonState.Released) action.Function(null, gameTime); } break; case Control.LeftTrigger: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Triggers.Left, gameTime); } else { if (_curGamePadState.Triggers.Left == 1.0f && _lastGamePadState.Triggers.Left < 1.0f) action.Function(null, gameTime); } break;
case Control.RightTrigger: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Triggers.Right, gameTime); } else { if (_curGamePadState.Triggers.Right == 1.0f && _lastGamePadState.Triggers.Right < 1.0f) action.Function(null, gameTime); } break; case Control.LeftShoulder: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.LeftShoulder, gameTime); } else { if (_curGamePadState.Buttons.LeftShoulder == ButtonState.Pressed && _lastGamePadState.Buttons.LeftShoulder == ButtonState.Released) action.Function(null, gameTime); } break; case Control.RightShoulder: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.Buttons.RightShoulder, gameTime); } else { if (_curGamePadState.Buttons.RightShoulder == ButtonState.Pressed && _lastGamePadState.Buttons.RightShoulder == ButtonState.Released) action.Function(null, gameTime); } break; case Control.LeftStick: if (action.Action < ActionType.Fire) { //convert to a direction if (_curGamePadState.ThumbSticks.Left != Vector2.Zero) { float degrees = ((float)(Math.Atan2(_curGamePadState.ThumbSticks.Left.X, _curGamePadState.ThumbSticks.Left.Y) * 57.2957795)); if (degrees < 0) { if (degrees >= -67) dir = (int)Direction.NorthWest; else if (degrees >= -112) dir = (int)Direction.West; else if (degrees >= -157) dir = (int)Direction.SouthWest; else dir = (int)Direction.South; } else { dir = (int)((int)(degrees / 45)); } action.Function((Direction)dir, gameTime); if (action.Action < ActionType.Rotate) moving = true; } else if (action.Action < ActionType.Rotate) moving = false; } else { if (_curGamePadState.ThumbSticks.Left != Vector2.Zero && _lastGamePadState.ThumbSticks.Left == Vector2.Zero) action.Function(_curGamePadState.ThumbSticks.Left, gameTime); } break; case Control.RightStick: if (action.Action < ActionType.Fire) { //convert to a direction if (_curGamePadState.ThumbSticks.Right != Vector2.Zero) { float degrees = ((float)(Math.Atan2(_curGamePadState.ThumbSticks.Right.X, _curGamePadState.ThumbSticks.Right.Y) * 57.2957795)); if (degrees < 0) { if (degrees >= -67) dir = (int)Direction.NorthWest; else if (degrees >= -112) dir = (int)Direction.West; else if (degrees >= -157) dir = (int)Direction.SouthWest; else dir = (int)Direction.South; } else { dir = (int)((int)(degrees / 45)); } action.Function((Direction)dir, gameTime); if (action.Action < ActionType.Rotate) moving = true; } else if (action.Action < ActionType.Rotate) moving = false; } else { if (_curGamePadState.ThumbSticks.Right != Vector2.Zero && _lastGamePadState.ThumbSticks.Right == Vector2.Zero) action.Function(_curGamePadState.ThumbSticks.Right, gameTime); } break;
case Control.DPadUp: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.DPad.Up, gameTime); } else { if (_curGamePadState.DPad.Up == ButtonState.Pressed && _lastGamePadState.DPad.Up == ButtonState.Released) action.Function(_curGamePadState.DPad.Up, gameTime); } break; case Control.DPadDown: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.DPad.Down, gameTime); } else { if (_curGamePadState.DPad.Down == ButtonState.Pressed && _lastGamePadState.DPad.Down == ButtonState.Released) action.Function(_curGamePadState.DPad.Down, gameTime); } break; case Control.DPadLeft: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.DPad.Left, gameTime); } else { if (_curGamePadState.DPad.Left == ButtonState.Pressed && _lastGamePadState.DPad.Left == ButtonState.Released) action.Function(_curGamePadState.DPad.Left, gameTime); } break; case Control.DPadRight: if (action.Action < ActionType.Fire) { action.Function(_curGamePadState.DPad.Right, gameTime); } else { if (_curGamePadState.DPad.Right == ButtonState.Pressed && _lastGamePadState.DPad.Right == ButtonState.Released) action.Function(_curGamePadState.DPad.Right, gameTime); } break; case Control.Back: case Control.Tab: case Control.Enter: case Control.CapsLock: case Control.Escape: case Control.Space: case Control.PageUp: case Control.PageDown: case Control.End: case Control.Home: case Control.Left: case Control.Up: case Control.Right: case Control.Down: case Control.Select: case Control.Print: case Control.Execute: case Control.PrintScreen: case Control.Insert: case Control.Delete: case Control.Help: case Control.D0: case Control.D1: case Control.D2: case Control.D3: case Control.D4: case Control.D5: case Control.D6: case Control.D7: case Control.D8: case Control.D9: case Control.A: case Control.B: case Control.C: case Control.D: case Control.E: case Control.F: case Control.G: case Control.H: case Control.I: case Control.J: case Control.K: case Control.L: case Control.M: case Control.N: case Control.O: case Control.P: case Control.Q: case Control.R: case Control.S: case Control.T: case Control.U: case Control.V: case Control.W: case Control.X: case Control.Y: case Control.Z: case Control.LeftWindows: case Control.RightWindows: case Control.Apps: case Control.Sleep: case Control.NumPad0: case Control.NumPad1: case Control.NumPad2: case Control.NumPad3: case Control.NumPad4: case Control.NumPad5: case Control.NumPad6: case Control.NumPad7: case Control.NumPad8: case Control.NumPad9: case Control.Multiply: case Control.Add: case Control.Separator: case Control.Subtract: case Control.Decimal: case Control.Divide: case Control.F1: case Control.F2: case Control.F3: case Control.F4: case Control.F5: case Control.F6: case Control.F7: case Control.F8: case Control.F9: case Control.F10: case Control.F11: case Control.F12: case Control.F13: case Control.F14: case Control.F15: case Control.F16: case Control.F17: case Control.F18: case Control.F19: case Control.F20: case Control.F21: case Control.F22: case Control.F23: case Control.F24: case Control.NumLock: case Control.Scroll: case Control.LeftShift: case Control.RightShift: case Control.LeftControl: case Control.RightControl: case Control.LeftAlt: case Control.RightAlt: case Control.BrowserBack: case Control.BrowserForward: case Control.BrowserRefresh: case Control.BrowserStop: case Control.BrowserSearch: case Control.BrowserFavorites: case Control.BrowserHome: case Control.VolumeMute: case Control.VolumeDown: case Control.VolumeUp: case Control.MediaNextTrack: case Control.MediaPreviousTrack: case Control.MediaStop: case Control.MediaPlayPause: case Control.LaunchMail: case Control.SelectMedia: case Control.LaunchApplication1: case Control.LaunchApplication2: case Control.OemSemicolon: case Control.OemPlus: case Control.OemComma: case Control.OemMinus: case Control.OemPeriod: case Control.OemQuestion: case Control.OemTilde: case Control.OemOpenBrackets: case Control.OemPipe: case Control.OemCloseBrackets: case Control.OemQuotes: case Control.Oem8: case Control.OemBackslash: case Control.ProcessKey: case Control.Attn: case Control.Crsel: case Control.Exsel: case Control.EraseEof: case Control.Play: case Control.Zoom: case Control.Pa1: case Control.OemClear: //we can use one case here if (action.Action < ActionType.Fire) { action.Function(_curKeyboardState.IsKeyDown((Keys)(action.ActionControl - (action.ActionControl - 1))), gameTime); } else { if (_curKeyboardState.IsKeyDown((Keys)(action.ActionControl - (action.ActionControl - 1))) && _lastKeyboardState.IsKeyUp((Keys)(action.ActionControl - (action.ActionControl - 1)))) action.Function(null, gameTime); } break;
case Control.LeftMouseButton: if (action.Action < ActionType.Fire) { action.Function(_curMouseState.LeftButton, gameTime); } else { if (_curMouseState.LeftButton == ButtonState.Pressed && _lastMouseState.LeftButton == ButtonState.Released) action.Function(null, gameTime); } break; case Control.MiddleMouseButton: if (action.Action < ActionType.Fire) { action.Function(_curMouseState.MiddleButton, gameTime); } else { if (_curMouseState.MiddleButton == ButtonState.Pressed && _lastMouseState.MiddleButton == ButtonState.Released) action.Function(null, gameTime); } break; case Control.RightMouseButton: if (action.Action < ActionType.Fire) { action.Function(_curMouseState.RightButton, gameTime); } else { if (_curMouseState.RightButton == ButtonState.Pressed && _lastMouseState.RightButton == ButtonState.Released) action.Function(null, gameTime); } break; case Control.MouseButton1: if (action.Action < ActionType.Fire) { action.Function(_curMouseState.XButton1, gameTime); } else { if (_curMouseState.XButton1 == ButtonState.Pressed && _lastMouseState.XButton1 == ButtonState.Released) action.Function(null, gameTime); } break; case Control.MouseButton2: if (action.Action < ActionType.Fire) { action.Function(_curMouseState.XButton2, gameTime); } else { if (_curMouseState.XButton2 == ButtonState.Pressed && _lastMouseState.XButton2 == ButtonState.Released) action.Function(null, gameTime); } break; } } if (!moving) { Stop(null, gameTime); } } _lastGamePadState = _curGamePadState; _lastKeyboardState = _curKeyboardState; _lastMouseState=_curMouseState;
} } |
Code Listing 45 – InputSystem class
There’s a lot going on here, so let’s take a closer look.
The first thing we see is the implementation of the ActionDelegate delegate. We have the eight types of actions for which we’ll do something. Notice that there’s an ActionDelegate for Stop. If the player isn’t doing one of the actions related to moving that we mentioned before, we stop the character from moving.
The next members are the current and previous states of the input devices we’ll handle.
After this, we have the implementation of our interface. We also provide a variety of methods for allowing the modification of mapped actions.
The Update method is the largest chunk of code in the class, as you would expect. After getting the current state of the input devices, if the input system is enabled, we go through all of our mapped actions, examining the current input to see if the conditions match to fire the function associated with that action.
If, after evaluating all the actions, it’s determined that the character is not moving, the Stop action function is called. We then set the current input states to the last input states.
The InputSystem instance is created in the Game class:
InputSystem input; protected override void Initialize() { . . .
input = new InputSystem(this); //we don't want to use this until we play the game or configure the controls input.Enabled = false; input.AddAction("Move", Control.LeftStick, ActionType.Move, null); input.AddAction("Move Foward", Control.DPadUp, ActionType.MoveForward, null); input.AddAction("Move Backward", Control.DPadDown, ActionType.MoveBackward, null); input.AddAction("Move Left", Control.DPadLeft, ActionType.MoveLeft, null); input.AddAction("Move Right", Control.DPadRight, ActionType.MoveRight, null); input.AddAction("Fire", Control.RightTrigger, ActionType.Fire, null); input.AddAction("Rotate", Control.RightStick, ActionType.Rotate, null); Components.Add(input); this.Services.AddService(typeof(IInputSystem), input); } |
Code Listing 46 – Input System Activation
The input system is enabled only when the player has started the actual gameplay. This happens in the Initialize method of the GameplayScreen class:
IInputSystem input = ((IInputSystem)ScreenManager.Game.Services.GetService(typeof(IInputSystem))); input.SetActionFunction("Move", Move); input.SetActionFunction("Move Foward", MoveForward); input.SetActionFunction("Move Backward", MoveBackward); input.SetActionFunction("Move Left", MoveLeft); input.SetActionFunction("Move Right", MoveRight); input.SetActionFunction("Fire", Fire); input.SetActionFunction("Rotate", Rotate); input.SetActionFunction("Stop", Stop); input.Enable(); |
Code Listing 47 – Input System Activation
The ActionDelegate parameter for the SetActionFunction method calls are methods in the GameplayScreen class:
private void Move(object value, GameTime gameTime) { _entityManager.MovePlayer((Direction)value); } private void MoveForward(object value, GameTime gameTime) { if (value != null) { if ((ButtonState)value == ButtonState.Pressed) { _entityManager.MovePlayer(_entityManager.GetPlayerDirection()); } } } private void MoveBackward(object value, GameTime gameTime) { if (value != null) { if ((ButtonState)value == ButtonState.Pressed) { Direction dir = _entityManager.GetPlayerDirection(); if (dir < Direction.South) dir += 4; else dir = (Direction)((int)Direction.NorthWest - (int)dir); _entityManager.MovePlayer(dir); } } } private void MoveLeft(object value, GameTime gameTime) { if (value != null) { if ((ButtonState)value == ButtonState.Pressed) { Direction dir = _entityManager.GetPlayerDirection(); if (dir < Direction.East) dir += 6; else dir = (Direction)((int)Direction.NorthWest - (int)dir); _entityManager.MovePlayer(dir); } } } private void MoveRight(object value, GameTime gameTime) { if (value != null) { if ((ButtonState)value == ButtonState.Pressed) { Direction dir = _entityManager.GetPlayerDirection(); if (dir < Direction.West) dir += 2; else dir = (Direction)((int)Direction.NorthWest - (int)dir); _entityManager.MovePlayer(dir); } } } private void Rotate(object value, GameTime gameTime) { if (value != null) { _entityManager.SetPlayerDirection((Direction)value); } } private void Stop(object value, GameTime gameTime) { _entityManager.StopPlayer(); } private void Fire(object value, GameTime gameTime) { _entityManager.PlayerFire(); } |
Code Listing 48 – Input System Action Methods
The EntityManager class takes care of resolving all the actions that the player takes. We have a bit of the EntityManager started already, and we’ll continue fleshing it out with the methods called here:
public void SetPlayerDirection(Direction dir) { if (_entities.Count > 0) { _entities[0].MoveDirection = dir; } } public void StopPlayer() { if (_entities.Count > 0) { _entities[0].Speed = 0.0f; } } public Direction GetPlayerDirection() { if (_entities.Count > 0) { return _entities[0].MoveDirection; } else return Direction.North; } public void MovePlayer(Direction dir) { if (_entities.Count > 0) { _entities[0].MoveDirection = dir; _entities[0].Speed = 3.0f; } } public void PlayerFire() { if (_entities.Count > 0) {
} } |
Code Listing 49 – EntityManager Action Methods
The methods include a sanity check to ensure we’re not attempting to set properties of a null object. This should never happen, but such validation is good practice.
If we move the character, we ensure his speed is set to the default. You could use a constant here, with other constants that could be used for a character that has had his speed enhanced to be faster or slower than normal. This could be done if you put powerups into the game.
Notice the empty if statement in the PlayerFire method. We’ll implement audio in the next chapter.
The last piece we need to implement is the InputState instance. That goes in the ScreenManager class, where it’s also updated and passed to the screens:
InputState _input = new InputState(); public override void Update(GameTime gameTime) { // Read the keyboard and gamepad. _input.Update(); . . . while (_screensToUpdate.Count > 0) { // Pop the topmost screen off the waiting list. GameScreen screen = _screensToUpdate[_screensToUpdate.Count - 1]; _screensToUpdate.RemoveAt(_screensToUpdate.Count - 1); // Update the screen. screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); if (screen.ScreenState == ScreenState.TransitionOn || screen.ScreenState == ScreenState.Active) { // If this is the first active screen we came across, // give it a chance to handle input. if (!otherScreenHasFocus) { screen.HandleInput(_input, gameTime); otherScreenHasFocus = true; } // If this is an active non-popup, inform any subsequent // screens that they are covered by it. if (!screen.IsPopup) coveredByOtherScreen = true; } } if (_traceEnabled) TraceScreens(); } |
Code Listing 50 – ScreenManager Updated Update Method
We need to add input handling to a few classes, starting with the the HighScoreScreen class:
public override void HandleInput(InputState input, GameTime gameTime) { if ((input.CurrentGamePadState.Buttons.B == Microsoft.Xna.Framework.Input.ButtonState.Pressed && input.LastGamePadState.Buttons.B == Microsoft.Xna.Framework.Input.ButtonState.Released) || (input.CurrentKeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Escape) && input.LastKeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys.Escape))) this.ExitScreen(); base.HandleInput(input, gameTime); } |
Code Listing 51 – HighScoreScreen HandleInput Method
The MessageBoxScreen class:
public override void HandleInput(InputState input, GameTime gameTime) { if (input.MenuSelect) { // Raise the accepted event, then exit the message box. if (Accepted != null) Accepted(this, EventArgs.Empty);
ExitScreen(); } else if (input.MenuCancel) { // Raise the cancelled event, then exit the message box. if (Cancelled != null) Cancelled(this, EventArgs.Empty); ExitScreen(); } } |
Code Listing 52 – MessageBoxScreen HandleInput Method
The GameplayScreen class:
public override void HandleInput(InputState input, GameTime gameTime) { if (input == null) throw new ArgumentNullException("input"); if (input.PauseGame) { ScreenManager.AddScreen(new PauseMenuScreen()); } } |
Code Listing 53 – GameplayScreen HandleInput Method
The MenuScreen class:
public override void HandleInput(InputState input, GameTime gameTime) { // Move to the previous menu entry? if (input.MenuUp) { _selectedEntry--; if (_selectedEntry < 0) _selectedEntry = _menuEntries.Count - 1; } // Move to the next menu entry? if (input.MenuDown) { _selectedEntry++; if (_selectedEntry >= _menuEntries.Count) _selectedEntry = 0; } // Accept or cancel the menu? if (input.MenuSelect) { OnSelectEntry(_selectedEntry); } else if (input.MenuCancel) { OnCancel(); } if (input.IsNewKeyPress(Keys.Left)) OnNewArrowDown(Keys.Left, _selectedEntry); else if (input.IsKeyDown(Keys.Left)) OnArrowDown(Keys.Left, _selectedEntry, gameTime);
if (input.IsNewKeyUp(Keys.Left)) OnArrowUp(Keys.Left, _selectedEntry); if (input.IsNewKeyPress(Keys.Right)) OnNewArrowDown(Keys.Right, _selectedEntry); else if (input.IsKeyDown(Keys.Right)) OnArrowDown(Keys.Right,_selectedEntry, gameTime); if (input.IsNewKeyUp(Keys.Right)) OnArrowUp(Keys.Right, _selectedEntry); } |
Code Listing 54 – MenuScreen HandleInput Method
Now that we have an input system, we need to give the player a way to change which controls map to the actions we’ve defined. We’ll add a new screen to allow this. Create a new class in the Screens folder and add the following:
class ControlsScreen : MenuScreen { private string[] _items; private int[] _selectedIndices; private bool _leftArrowDown = false; private bool _rightArrowDown = false; private float _leftArrowDownTime = 0.0f; private float _rightArrowDownTime = 0.0f; private List<MappedAction> _actions; public ControlsScreen() { } public override void Initialize() { base.Initialize(); //get the current configuration and load the menu _actions = ((IInputSystem)ScreenManager.Game.Services.GetService(typeof(IInputSystem))).MappedActions; _selectedIndices = new int[_actions.Count]; int count = 0; foreach (MappedAction action in _actions) { _menuEntries.Add(action.Name + ": "); _selectedIndices[count] = (int)action.ActionControl; count++; } _menuEntries.Add("Back"); //populate items array with all available controls _items = new string[System.Enum.GetNames(typeof(Control)).Length]; count = 0; //Loop through Keys enum foreach (int value in System.Enum.GetValues(typeof(Control))) { _items[count] = System.Enum.GetName(typeof(Control), value); count++; } } public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); int count = 0; string entry; for (count = 0; count < _actions.Count;count++ ) { entry = _actions[count].Name + ": " + _items[_selectedIndices[count]]; _menuEntries[count] = entry; } } protected override void OnSelectEntry(int entryIndex) { //increment and check item if (entryIndex == 7) { // Go back to the main menu. ExitScreen(); } } protected override void OnArrowUp(Keys arrow, int entryIndex) { int index = _selectedIndices[entryIndex]++; switch (arrow) { case Keys.Left: index--; if (index < 0) index = _items.Length - 1; break; case Keys.Right: index++; if (index > _items.Length - 1) index = 0; break; } _selectedIndices[entryIndex] = index;
} protected override void OnArrowDown(Keys arrow, int entryIndex, GameTime gameTime) { int index = _selectedIndices[entryIndex]++; switch (arrow) { case Keys.Left: if (_leftArrowDown) { _leftArrowDownTime += gameTime.ElapsedGameTime.Milliseconds; if (_leftArrowDownTime > 250.0f) { index--; _leftArrowDownTime = 0.0f; } } else { _leftArrowDown = true; _leftArrowDownTime = 0.0f; } break; case Keys.Right: if (_rightArrowDown) { _rightArrowDownTime += gameTime.ElapsedGameTime.Milliseconds; if (_rightArrowDownTime > 250.0f) { index++; _rightArrowDownTime = 0.0f; } } else { _rightArrowDown = true; _rightArrowDownTime = 0.0f; } break; } if (index < 0) index = _items.Length - 1; else if (index == _items.Length) index=0; _selectedIndices[entryIndex] = index; } protected override void OnCancel() { ExitScreen(); } public override void ExitScreen() { //save the controls configuration MappedAction action; for (int count = 0; count < _actions.Count; count++) { action = _actions[count]; action.ActionControl = (Control)_selectedIndices[count]; } ((IInputSystem)ScreenManager.Game.Services.GetService(typeof(IInputSystem))).MappedActions = _actions; base.ExitScreen(); } } |
Code Listing 55 – ControlsScreen Class
We now have a way to control our character. The last system we’ll be adding is audio to handle sounds and music.