Saturday, August 27, 2011

How to write an input handler library for XNA 4.0

As always source code included at the end of the post

I have been gone for a while, real work had me tied up and in NYC for a while but I am home finally and I bring updates. Most of you reading my blog, like me, are interested in using the XNA framework to develop games. A big part of that is writing the graphics code and the other exciting bits. There is also all of the other code we have to write. Stuff state management, menu screens, handling input... all stuff that is very important to making a good game, but usually not as interesting as making explosions.

Fortunately XNA makes doing this a whole lot easier but it does not make the problem go away completely. This means that, for example, we find ourselves writing a whole lot of code that looks like this

KeyboardState currentState = Keyboard.GetState();
if (currentState.IsKeyDown(Keys.Space) && lastState.IsKeyUp(Keys.Space))
{
	// do something cool
}

// rinse & repeat for all of our other input keys

lastState = currentState;

This isn't too bad... BUT there are a couple of problems with this. First once you write this code it isn't really the easiest to make configurable. The best option for that would be storing the target action key, in this example Keys.Space in a variable and load that variable in some configurable way someplace else in our game. The problem with this, which is related to our other big drawback with handling input like this, is that we could potentially have these input blocks scattered across numerous different game component Update() calls. That is exactly what the other big drawback to this approach is. Changing our input handling means we potentially need to visit each of our game component classes.

What we really want to do here is be able to separate input handling from actually doing the stuff we want to do when the input happens. An excellent model for this is the event based approach for handling things like input. Well, XNA doesn't do event based input it is all polled input. Each game update loop we poll our input sources and look for specific changes. This doesn't mean we can't emulate event based input though, and when we do that we find we now have a nice separation between handling the input and detecting the input. XNA makes this even more beautiful by registering the input handler as a game component it all just starts to work.

So with all this in mind, and the strong desire to not have to write any more input handling code. I have implemented a GameComponent that can handle all of my input detection in a nice generalized way and fire off registered events when the right input actions occur. In addition the input handler provides a built in mechanism for loading input mapping from an external XML file. To make this even more useful it has been implemented as a Game Library so that I can just include it in any of my other games.

The rest of this post will cover using InputHandler in a game. In my next post I will tear apart the InputHandler for anyone that is interested in learning more about how I put it together.

To demonstrate the use of the InputHandler I will create a game that displays two spheres and lets the player move them around with various inputs.

First the input mapping I will be using for this game, this isn't strictly required but why do extra work if i don't have to. The input mapping is stored in an XML file with the following format:

<?xml version="1.0"?>
<InputMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Functions>
    <Function>
      <FunctionName>Obstacle.LeftMouse</FunctionName>
      <InputActions>
        <KeyboardInput>
          <Key>L</Key>
          <Modifiers>
            <Key>RightControl</Key>
          </Modifiers>
        </KeyboardInput>
        <MouseInput>
          <Button>LeftButton</Button>
          <Modifiers>
            <Key>RightControl</Key>
          </Modifiers>
        </MouseInput>
      </InputActions>
    </Function>
  </Functions>
</InputMap>

Our XML file defines an InputMap that contains Functions collection where we define Function that define a FunctionName and a InputActions. InputActions are a list of either KeyBoardInput or MouseInput elements. KeyboardInput and MouseInput elements may optionally include Modifies. When we instantiate an InputHandler with a file name the named XML file will be loaded into an InputMap that can be referenced by game code to map game functions to input configuration.

If you haven't done so already now would be a good time to download the GameUtility.dll.

Create a new project right click References in the solution explorer select "Add Reference". Change the Add References dialog tab to Browse and find the GameUtility.dll, select it and add it to the project.

That's it for the set up, now we will make our changes to the game class.

First add a private member to the game class to hold our InputHandler and a boolean value so we can keep track of our first time we call update.

/* Define our input handler.  We will make this a global
 * member incase we ever want to refer to it again in the
 * game class.
 */
InputHandler inputHandler;

/* Keep track of the first time
 * we call our update method
 */
private bool firstUpdate = true;

Now we are going to add a bit of code to the Initialize method. This will add our InputHandler to the game components and game services element. Adding the InputHandler as a GameComponent will cause its Initialize and Update methods to be called during appropriate moments in the games life cycle. Adding the InputHandler as a game service is optional but it makes it easier to access the handler from other components.

protected override void Initialize()
{
    /* Make our mouse pointer visible
     */
    this.IsMouseVisible = true;

    /* get a new input handler
     */
    inputHandler = new InputHandler(this, "test.xml");

    /* add it as a service so we can access it from our other game
     * game components and add it as a component so that its update
     * method is called each game update cycle
     */
    Services.AddService(typeof(InputHandler), inputHandler);
    Components.Add(inputHandler);

    /* add a new player and obstacle component
     */
    Components.Add(new PlayerComponent(this));
    Components.Add(new ObstacleComponent(this));

    /* call base initialize to init our other game components
     */
    base.Initialize();
}

Here you see we are also adding a PlayerComponent and ObstacleComponent here, we will get to those implementations later.

We are almost done with the game class. First a small change to LoadContent...

/* Add the sprite batch as a service so it can be accessed from the
* other game components
*/
Services.AddService(typeof(SpriteBatch), spriteBatch);

Like the InputHandler adding the SpriteBatch as a service will make it more accessible to our other game components.

Now the final change to the game class.
if (firstUpdate)
{
    inputHandler.Started = true;
    firstUpdate = false;
}

This just tells our InputHandler that we are ready for it to start handling input as soon as the first game update cycle processes. Of course in a real game we could turn on, or off for that matter, input handling how ever we like.

That's it we now have a running game that will handle input for us, we just need to register the actions we want to take.

In order to make this more interesting add a new DrawableGameComponent to your game project.

/* Players in this game have a texture and a position
 */
private Texture2D texture;
public Vector2 position;

/* Keep a reference to our input handler
 * so that we do not have to keep looking it
 * up from the Game.Services property
 */
private InputHandler inputHandler;

/* Some other useful information about our
 * player
 */
private bool MovingLeft = false;                    // Are we moving left?
private float buttonSpeed = 100.0f;                 // How many pixels per second do we move?
private bool MouseDrag = false;                     // Are we being drug by the mouse?
private Vector2 LeftClickOffset = Vector2.Zero;     // Were did the mouse grab us in relation to our corner?

Here we are just adding a bunch of class members to store things like out texture, position, a reference to the InputHandler (which we will look up a little later) and a few other values to manage the movement of the object.

/// 
/// Function to handle the left movement key
/// 
/// Provides a snapshot of timing values.public void moveLeft(GameTime gameTime)
{
    /* We are going to demonstrate using key down and key up
     * triggers to force one discrete action per key press
     */

    /* If we are already in our move left state do not
     * do anything else
     */
    if (!MovingLeft)
    {
	/* We started moving left so set our move left state
	 * to true
	 */
	MovingLeft = true;

	/* and move us by our time scaled offset
	 */
	position += new Vector2(
	    -buttonSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds,
	    0.0f);
    }
}

/// 
/// This method is called on our mapped left function key up event.
/// When the key is released we will leave our move left state.
/// 
/// public void stopLeft(GameTime gameTime)
{
    /* the left button was just released so leave the move
     * left state.
     */
    MovingLeft = false;
}

/// 
/// For our right movement function we will continue to move
/// as long as our mapped right function key is held down.
/// 
/// Provides a snapshot of timing values.public void moveRight(GameTime gameTime)
{
    /* move by our time scaled off est
     */
    position += new Vector2(
	buttonSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds,
	0.0f);
}

/// 
/// For our up movement function we will continue to move
/// as long as our mapped up function key is held down.
/// 
/// Provides a snapshot of timing values.public void moveUp(GameTime gameTime)
{
    /* Move up by our time scaled offset
     */
    position += new Vector2(
	0.0f,
	-buttonSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds);
}

/// 
/// For our down movement function we will continue to move
/// as long as our mapped down function key is held down.
/// 
/// Provides a snapshot of timing values.public void moveDown(GameTime gameTime)
{
    /* Move down by our time scaled offset
     */
    position += new Vector2(
	0.0f,
	buttonSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds);
}

As you can see these are all simple functions that are going to perform a single distinct action when called. You may have noticed though that the method moveLeft seems a little more complicated. This is just to demonstrate how we can leverage the fact that we actually get two events with each key press, a down event and up event.

We will also add a method to handle mouse input. This method will just make sure that the mouse pointer is in the area of the screen that sprite is currently in and then set the drag state to true. Just as with keys releasing a mouse button will fire a button up event that fire the mouseReleaseLeft method.

/// 
/// We can drag and drop our player around the screen.  If the mapped
/// button click happens over our player sprite enter our MouseDrag
/// state and remain in it until our button up event is processed.
/// 
/// Provides a snapshot of timing values.public void mouseClickLeft(GameTime gameTime)
{
    /* Get the current mouse position
     */
    Vector2 mousePos = inputHandler.MousePosition;

    /* If we are not already in drag mode
     * and our mouse click happens inside the rectangle of
     * our player sprite...
     */
    if (!MouseDrag
	&& mousePos.X >= position.X && mousePos.X <= (position.X + texture.Width)
	&& mousePos.Y >= position.Y && mousePos.Y <= (position.Y + texture.Height))
    {
	/* Enter the mouse drag state
	 */
	MouseDrag = true;

	/* And figure out the offset between the top
	 * left corner of the player sprite and the pointer
	 * at the time of the click event.
	 */
	LeftClickOffset = position - mousePos;
    }
}

/// 
/// When our mapped button is released we will leave the
/// mouse drag state.
/// 
/// Provides a snapshot of timing values.public void mouseReleaseLeft(GameTime gameTime)
{
    /* Leave the mouse drag state
     */
    MouseDrag = false;
}

Now that we have our methods that will actually handle the input we just need to tie them to our input handler. This is done in the Initialize method.

public override void Initialize()
{
    // Start the player at 0,0
    position = Vector2.Zero;

    // Get our input handler from the Game.Services property
    inputHandler = Game.Services.GetService(typeof(InputHandler)) as InputHandler;

    /* Register our key press functions.  We have a key don and key
     * up functions for the left key press and key down functions for
     * right, up and down.
     * 
     * Look up the actual key from the InputMap in our input handler.  This
     * lets us externally configure our input options.
     */
    inputHandler.MapFunctionEntry(
	inputHandler.InputMap.GetFunctionEntries("Player.Left"),
	this.moveLeft, this.stopLeft);
    inputHandler.MapFunctionEntryDown(
	inputHandler.InputMap.GetFunctionEntries("Player.Right"),
	this.moveRight);
    inputHandler.MapFunctionEntryDown(
	inputHandler.InputMap.GetFunctionEntries("Player.Up"),
	this.moveUp);
    inputHandler.MapFunctionEntryDown(
	inputHandler.InputMap.GetFunctionEntries("Player.Down"),
	this.moveDown);

    /* Register our mouse click functions for drag and drop to work correctly
     * we need a button down and a button up function
     */
    inputHandler.MapFunctionEntry(
	inputHandler.InputMap.GetFunctionEntries("Player.LeftMouse"),
	this.mouseClickLeft, this.mouseReleaseLeft);

    base.Initialize();
}

In Initialize we register our input handler methods by looking up some input definitions from our input map loaded in the InputHandler. We will pass these input definitions to our InputHandler along with either one method to be called on the key or button down event or two methods one for the down action one for the up.

Finally just two small additions to the Update and Draw methods.

In update we are just going to change the player position to follow the mouse pointer if we are in the mouse drag state.

public override void Update(GameTime gameTime)
{
    /* If we have been clicked on by the mouse and not
     * yet released update our position to follow the
     * mouse pointer
     */
    if (MouseDrag)
    {
	/* Our new position will be the current mouse position
	 * offset by the distance from the player corner of the
	 * original click.
	 */
	position = LeftClickOffset + inputHandler.MousePosition;
    }

    /* Since keyboard input will be handled by the InputComponent nothing else
     * to do here
     */

    base.Update(gameTime);
}

and in draw we just need to draw our sprite out the screen.

public override void Draw(GameTime gameTime)
{
    /* Get our sprite batch from the Game.Services property
     */
    SpriteBatch spriteBatch = Game.Services.GetService(typeof(SpriteBatch)) as SpriteBatch;

    /* Draw the player sprite
     */
    spriteBatch.Begin();
    spriteBatch.Draw(texture, position, Color.White);
    spriteBatch.End();

    base.Draw(gameTime);
}

That's it. Running the game now will display our player sprite to the screen and allows it to respond to input. If we want to change our game around so that it responds to different button or key presses it's now as easy as editing the single XML file our input is mapped from.

To make this demo more interesting I have also implemented an ObstacleComponent that is pretty much identical to the PlayerComponent just to give us a wider range of input responses.

Feel free to use this InputHandler in your own projects or just use it to learn from.
Enjoy.

GameUtilities.dll
InputMappingFramework.zip

No comments:

Post a Comment