Sunday, February 1, 2009

On Input and Managers

*Disclamer* I am assuming that you already have a basic knowlege of XNA. I'm not going to explain the code that is generated for you when you create a new XNA project. There enough examples doing that already.


It is a (good) standard practice for the first line of the Update code to call another method called HandleInput. Most of the samples I have seen have HandleInput code along the lines of:
KeyboardState keyboardState = Keyboard.GetState();
GamePadState gamepadState = GamePad.GetState(PlayerIndex.One);

// Exit the game when back is pressed.
if (gamepadState.Buttons.Back == ButtonState.Pressed)
Exit();

bool continuePressed =
keyboardState.IsKeyDown(Keys.Space) ||
gamepadState.IsButtonDown(ContinueButton);
(Taken from the Platformer Starter Kit, included with XNA Games Studio 3.0)

Does this work? Yes. So what are the problems with it..?
  • On the XBox 360 it wont work if if the player's controller is not plugged into the first port (they won't be player 1).
  • It is unlikely that people are going to be using a Keyboard and GamePad at the same time - so why are we checking both?
  • If we want to add support for another input device then we would have to go through every HandleInput method to make the changes (mouse, guitar, Buzz© controller anyone?)
  • Personally, when I'm playing games, I like to be able to configure the controlls to how I like them. When using a Keyboard, what do we have to press to jump? The space bar? Up arrow? Either of them should work?
Thinking back to when I first learnt about the OO Principles, this is clearly going the opposite way from Encapsulation. The HandleInput method of our Spaceship class shouldn't need to worry about what the current key mapping is.

I started by creating an interface IInputManager. I wanted to make it easy to create a manager for the GameController and Keyboard so I gave my interface methods corisponding to each of the XBox 360 controller buttons (with several overloads)


Namespace Input
Public Interface IInputManager

Function MainMovement() As Vector2
Function MainSelectPressed() As Boolean
Function MenuMovement() As Vector2
Function MenuMainMovement() As Vector2
Function SecondaryMovement() As Vector2
Function SecondarySelectPressed() As Boolean
Function LeftTrigger() As Single
Function LeftShoulderPressed() As Boolean
Function BackPressed() As Boolean
Function StartPressed() As Boolean
Function RightTrigger() As Single
Function RightShoulderPressed() As Boolean
Function APressed() As Boolean
Function BPressed() As Boolean
Function XPressed() As Boolean
Function YPressed() As Boolean
Function MainSelectDown() As Boolean
Function SecondarySelectDown() As Boolean
Function LeftShoulderDown() As Boolean
Function BackDown() As Boolean
Function StartDown() As Boolean
Function RightShoulderDown() As Boolean
Function ADown() As Boolean
Function BDown() As Boolean
Function XDown() As Boolean
Function YDown() As Boolean
Sub Update(ByVal elapsedTime As Single)
End Interface ' IInputManager
End Namespace ' Input

From this interface I created two implementations, KeyboardInputManager and GamePadInputManager.

Namespace Input
Public Class KeyboardInputManager
Implements iInputManager

Public Sub New()
' Default Key Mapping
With mKeys
.Add("MainMovementUp", Keys.Up)
.Add("MainMovementDown", Keys.Down)
.Add("MainMovementLeft", Keys.Left)
.Add("MainMovementRight", Keys.Right)
.Add("MainSelect", Keys.Q)

.Add("SecondaryMovementUp", Keys.W)
..... etc.
End With
End Sub

Public Function Back() As Boolean Implements iInputManager.BackPressed
Return IsNewKeypress(mKeys("Back"))
End Function

Public Function LeftShoulder() As Boolean Implements iInputManager.LeftShoulderPressed
Return mCurrentKeyboardState.IsKeyDown(mKeys("LeftShoulder"))
End Function

Public Function MainMovement() As Microsoft.Xna.Framework.Vector2 Implements iInputManager.MainMovement
Dim UpDown As Single = 0.0F
Dim LeftRight As Single = 0.0F

With mCurrentKeyboardState

If .IsKeyDown(mKeys("MainMovementUp")) Then
UpDown = -1.0F
ElseIf .IsKeyDown(mKeys("MainMovementDown")) Then
UpDown = 1.0F
End If
If .IsKeyDown(mKeys("MainMovementLeft")) Then
LeftRight = -1.0F
ElseIf .IsKeyDown(mKeys("MainMovementRight")) Then
LeftRight = 1.0F
End If

End With

Return New Vector2(LeftRight, UpDown)
End Function

Public Sub Update(ByVal elapsedTime As Single) Implements iInputManager.Update
mPreviousKeyboardState = mCurrentKeyboardState
mCurrentKeyboardState = Keyboard.GetState(PlayerIndex.One)
End Sub

Private Function IsNewKeypress(ByVal key As Keys) As Boolean
Return mCurrentKeyboardState.IsKeyDown(key) AndAlso Not mPreviousKeyboardState.IsKeyDown(key)
End Function

This is far from complete but is a good start. All out input code is now in the one place. We can easily change it without disturbing our existing game code. Our HandleInput methods can now follow the format HandleInput(input as Input.IInputManager).

This class needs some default action (return False) if there is no mapping for a particular key string/name. Also a method for Saving and Loading keymapping from XML files.

Some other ideas I had regarding input:
  • KeyboardInputManager & GamePadInputManager could Inherit from GameComponent.
  • Or even better - Inherit from an abstract class InputManager that Inherits from GameComponent.

  • Create an InputDeviceManager that would maintain a list of available input devices based on what devices are connected ("If GamePad.GetState(PlayerIndex.One).IsConnected Then") or other ones manually added.
  • This would allow selection of the currently active device or devices (e.g. keyboard and mouse).
Note: when I created these classes, I had not experimented at all with getting input from the mouse. The interface may need to be tweaked to make it easy to add a MouseInputManager.



The main point I want to get across is the benefits of Encapsulation and Abstraction. Take all the input related code out of your classes and put it into dedicated input classes. Put an interface inbetween so that your classes do not need to know or care about the implementation details.

Of course this does not just relate to Input! The same principles apply to all parts of your game. Look for areas that can be isolated. Create generic Manager classes. It will initially seem like more work but it is worth it in the long run. It makes updates a lot easier (especially when you can use the same set of Manager Classes for multiple games!).

Some examples of Managers that I can think of (and may eventually blog about):
  • Input Manager
  • InputDeviceManager
  • AudioManager
  • ScreenManager
  • AnimationManager (2D or 3D)
  • TexturedQuadManager

Till next time
Happy Coding =)
G

No comments: