Wednesday, April 15, 2009

Input Wrangling Part 1

Input is one, if not the most critical parts of a game, bad controls can ruin an otherwise great game. Fortunately XNA makes getting at game controller states very easy with statically available objects such as GamePad, Keyboard and Mouse providing state information. What XNA does not provide is a way to map this raw hardware state information into some sort of control scheme. I know that being able to remap input configuration is not something console gamers are used to, or would use if it was available. But I know I'm going to be playing with the control setup throughout development and I would love avoid having to go to each place a control is referenced in the code to make changes each and every time.

Apparently the developers of Spacewar felt the same way as the project includes some control mapping. But it always felt somewhat wrong to me, more inconvenient than it should. It didn't really strike me what was wrong with Spacewars control mapping implementation until I starting thinking about how I would change it. The fact is, it seems to be written backwards. To understand what I mean by this let's look at a little design philosophy.

Every software program is divided into sections to various degrees. In games you typically have the graphics engine, sound, game logic, AI, UI, data access etc. etc. and each of these sections relate to one another in certain ways. One way to look at these interaction is to assign rolls to the parts of the program which define how they relate. One common example are the Service/Client rolls. For example the data access code for a game (the content pipeline) provides a Service to the other parts of the game who are it's Clients. The graphics engine Client can make a request of the content pipeline Service for a given graphic image and the Service will provide it.

Thinking about it this way can help you design your systems to be most functional. One important fact about the Client/Service relationship to keep in mind is that the Client always defines the interactions, the Client has needs and the Service is to provide for those needs as conveniently as possible. If the Service starts imposing rules on the Client the behavior of the Client (in this case the game) will suffer.

While Microsoft's input state objects are easy to use they are not necessarily convenient when you start adding multiple players and different control types, and because they are properties up to 3 deep (e.g. gpSate.ThumbSticks.Left.Y) they do not lend themselves well to abstraction. Any input service should hide this inconvenience. Spacewars implementation does nothing for this, in fact it imposes it's own almost as restrictive format (XInputHelper.GamePads[player].ThumbStickLeftY). The X which allows player indexing and keyboard key to GamePad butting one-to-one mapping but little else. In short the XInputHelper and GamePadHelper do a fair amount of work to provide relativly little benefit to it's client, the game.

My goal here is to walk us through designing an Input Service called the InputWrangler that provied more benifites to the game with less work for us. Follow me now as we take a mental walk through an informal design session...

Problem: My game has 4 thrusters that need to be turned off and on via some user input, input over a network, or possibly by an AI bot. Multiple inputs should be able to trigger a given thruster event (Dpad buttons, mouse control, keyboard keys, etc. etc.). There will be up to 4 different players. The game code should be able to do something as simple as Inputs.Player1.ThrusterUp and not care how that value got set.

Lets say these thrusters are controls, the game can have as many controls as it needs like Thrusters, Guns, Jump, Menu etc. etc. Lets call the input devices triggers, so the GamePad Left Shoulder button is a trigger just as the Enter key on the keyboard is a trigger and we can make an AI hook into the input service another kind of trigger.

Solution:  The client of the service (the game),  has Controls which need to have multiple Triggers.

What the Game needs to know:
  • What the value of a given control is

What the Game does not need to know:
  • What trigger(s) are effecting a given control

What the InputWrangler needs to know:
  • What controls there are
  • What triggers are available
  • Which triggers will affect which controls

What the InputWrangler does not need to know
  • How the controllers are used

It's okay for the game to intrinsically know what  the controls are, they are part of it's basic structure. But it is not okay for the InputWrangler to have built in controls because different parts of the game have different control needs and they shouldn't have to deal with each others inputs. For example the Menu screen needs a Select control but doesn't need the Thruster controls from the GamePlay screen.

Lets look at an example of what my current game play needs, in it's simplest state with only two controls

Game Play Controllers & Triggers

ThrusterUp
  • Trigger Gamepad DPadUp
  • Trigger Gamepad  ThumbStickLeftY
  • Trigger Keyboard UpArrow
 ThrusterDown
  • Trigger Gamepad DpadDown
  • Trigger Gamepad ThumbStickLeftY
  • Trigger Keyboard DownArrow

We need to get this information into the InputWrangler, and we need to do it in such a way that different screens for the game can define different control setups. This looks like a good job for configuration XML files. Here's what I would like the control file for the above configuration to look like:

<InputSettings>
  <Inputs>
    <Players>
      <Player>
        <Controls>
          <Control name="ThrustUp">
            <Triggers>
              <Trigger controller="GamePad" value="DPadUp"/>
              <Trigger controller="GamePad" value="ThumbSticksLeftY"/>
              <Trigger controller="Key" value="Up"/>
            </Triggers>
          </Control>
          <Control name="ThrustDown">
            <Triggers>
              <Trigger controller="GamePad" value="DPadDown"/>
              <Trigger controller="GamePad" value="ThumbSticksLeftY"/>
              <Trigger controller="Key" value="Down"/>
            </Triggers>
          </Control>
        </Controls>
      </Player>
    </Players>
  </Inputs>
</InputSettings>

While the simple menu screen configuration would look like this (you can only hit Play right now):

<InputSettings>
  <Inputs>
    <Players>
      <Player>
        <Controls>
          <Control name="Play">
            <Triggers>
              <Trigger controller="GamePad" value="A"/>
              <Trigger controller="GamePad" value="X"/>
              <Trigger controller="Key" value="Enter"/>
              <Trigger controller="Key" value="Space"/>
              <Trigger controller="Mouse" value="LeftButton"/>
            </Triggers>
          </Control>
        </Controls>
      </Player>
    </Players>
  </Inputs>
</InputSettings>

Next: InputWrangler implementation, the joys and dangers of C# delegates.

No comments:

Post a Comment