Tuesday, 18 May 2010

Input management in XNA

Managing input in even the simplest game can result in a mess of confused lines of code scattered across the entire project. I have started to write an InputManager class that can be used to easily set up and use during the game.

There will be two main parts to the class. The first part is how to set it up during game loading, and the second is how to use it during the game to check what the player is doing. I use the concept of "Actions", which can describe a particular action the user wants to do, for example selecting a menu item, asking a character to jump, or steering a car to the left.


The InputManager class itself is quite simple, it stores a list of actions, has methods to create new actions and access existing ones, and of course has the standard Update method that in turn updates the state of each action:
public class InputManager
    {
        List<action> actions = new List<action>();

        public void AddAction(String ActionName)
        {
            actions.Add(new Action(this, ActionName));
        }

        public Action this[String ActionName]
        {
            get
            {
                return actions.Find((Action a)=>{return a.Name == ActionName;}); 
            }
        }

        public void Update()
        {
            KeyboardState kbState = Keyboard.GetState(currentPlayer);
            GamePadState gpState = GamePad.GetState(currentPlayer);

            foreach (Action a in actions)
                a.Update(kbState, gpState);
        }

    }

As you can see above, each Action is updated every frame and is passed the current keyboard and gamepad state. The variable currentPlayer should be set in the application to the active player (this can be determined by asking the player to press a "Start" button at the beginning of the game, and checking which controller has the pressed button).

The Action class is a bit longer. It contains a list of keys and a list of buttons that are associated with the action. It also contains two booleans to store the currents state (either pressed or not) and the previous state. The previous state is used to determine if the action has just been pressed, ie "tapped", rather than held down. There are also methods for adding keys and buttons to the action. Here is the complete code:
public class Action
    {
        String name;
        List<keys> keyList = new List<keys>();
        List<buttons> buttonList = new List<buttons>();
        InputManager parent = null;
        bool currentStatus = false;
        bool previousStatus = false;

        public bool IsDown { get { return currentStatus; } }
        public bool IsTapped { get { return (currentStatus) && (!previousStatus); }}
        public String Name { get { return name; } }

        public Action(InputManager p, string n)
        {
            parent = p;
            name = n;
        }

        public void Add(Keys key)
        {
            if (!keyList.Contains(key))
                keyList.Add(key);
        }

        public void Add(Buttons button)
        {
            if (!buttonList.Contains(button))
                buttonList.Add(button);
        }

        internal void Update(KeyboardState kbState, GamePadState gpState)
        {
            previousStatus = currentStatus;
            currentStatus = false;
            foreach (Keys k in keyList)
                if (kbState.IsKeyDown(k))
                    currentStatus = true;
            foreach (Buttons b in buttonList)
                if (gpState.IsButtonDown(b))
                    currentStatus = true;
        }
    }

The Update method is marked as internal, so that it cannot be called externally from the InputManager assembly. The Update loops through all the associated keys and buttons, and if it finds any of them pressed it sets the currentStatus to true.

To use the input manager class in a game, first the actions need to be set up:
InputManager im = new InputManager();
im.AddAction("Menu Up");
im["Menu Up"].Add(Keys.Up);
im["Menu Up"].Add(Buttons.DPadUp);

im.AddAction("Move Forwards");
im["Move Forwards"].Add(Keys.Up);
im["Move Forwards"].Add(Keys.W);
im["Move Forwards"].Add(Buttons.DPadUp);

In this way it is possible to add as many keys and buttons to each action, and have multiple actions associated with each button. In a more complex game targeted for Windows you probably would like to give the player the option to change the controls, for this you would need to add some kind of Remove or Reset method to the action class, to clear the already assigned controls.

To use the input manager in the game loop is then very straightforward, and does not depend on any game settings or what keys or buttons are assigned:
if(im["Menu Up"].IsTapped) SelectionIndex--;
if(im["Move Forwards"].IsDown) playerSpeed += 0.5;

Notice how for the menu controls the IsTapped property is read, and for the moving control the IsDown property is read. In the menu system when the player presses a key or button, we only want to act once while they are pressing the button and not every frame (otherwise it would be very hard to navigate the menu system). In the game itself however, the player expects to speed up continuously while the move forward button is being held down.

Extensions to the class could provide some auto-repeat functionality, which would be useful for navigating long lists in menus or some scrolling situations. What I will also cover in a later post is incorporating analog stick controllers into the Action class, so the player has the choice of using an analog controller for actions in addition to the digital buttons.

3 comments:

  1. just wondering, would this work for mutiple controllers input? or just the keyboard and 'a' controller?

    looking for a way to manage input for 4 xbox controllers at once during gameplay/character selection.

    ReplyDelete
  2. I don't see why it couldn't be expanded to work with multiple controllers. You'd need to decide first if you wanted identical controls for all controllers, or allow individuals to customise their controls. Instead of storing and using GamePadState you'd just use an array of GamePadState's, one for each controller.

    ReplyDelete
    Replies
    1. Thanks for the quick reply, I'll look into it.

      Delete