2015/08/05

Mapping Input to a Virtual Controller

The input system I built for my engine is fairly simple, but it involves a fair amount of code as well.

As I mentioned last time, it revolves around what's essentially a virtual SNES controller, with just twelve digital buttons. There's two concrete input mappers, one that maps keyboard input to that virtual controller, another one for game pad input.
Then there's two additional configuration classes, to allow rebinding either input method. Then there's all the details, primarily initialization in this case.

The Polling Interface

Let's begin with the abstract class InputMapper. It's essential function will be to provide a button state for a given button. It's pretty much the only thing it'll ever really do, so I decided to make that an index. The return type is a custom [Flags] enum ButtonState, which has one flag for the current state and the previous state, so it also encompasses Released and Pressed events:

The enum Buttons doesn't have to be a flag field, but for convenience, we should be able to cast it to an integer, for use as an index into an array. You'll see why in a moment, but essentially, we should be explicit about that. No confusion how to use any such arrays then.

This leaves us with this bit of code for the InputMapper:

That should clear you up on why Buttons had to be array compatible, but how do we fill the state array?
For one, we should only fill it once, before we actually need any input, so we will call an Update method on the InputManager, before we get to the logical parts of the game loop. Instead of making everyone who implements the InputManager deal with ButtonState, however, we'll fill the array in an abstract way, and only have the implementer tell us whether a button is down. Before we can do that, the implementer will want his input data, though.
That code looks like this. The loop might be a little confusing there, so let me explain: Essentially, for each value in Buttons, we right shift the current state by one bit, and OR in the current state. This works because the ButtonState enum is a field of flags, with the lowest value bit representing the previous button state and the next lowest bit representing the current state. Right shifting by one bit will completely delete the previous state and move the current state into the previous state. The four possible combinations of previous and current state make up the enumeration.

Mapping Game Pad Input

To map game pad input to our virtual controller, we need to implement a class derived from InputMapper. The GamePadMapper only needs to implement PollState, which should retrieve and store a GamePadState instance, and IsButtonDown(), which should defer to GamePadState.IsButtonDown() with an appropriate parameter.
Figuring out that parameter is the real mapping. I used another array of length 12 for this, with the Button, cast to an int, as the index. GamePadState.IsButtonDown() takes Buttons as well, however that's Microsoft.Xna.Framework.Input.Buttons. Hooray for namespaces!

However, this just defers the problem: We still have to fill the array of Xna Buttons to actually map any input. And that's when we go back to our static class Configuration.
First of all, we need to add a GamePadConfigurationSection, which defines lots of properties like this:
Yeah, I copy/pasted twelve of these.

The reason I didn't use an array in the configuration is two-fold: I just didn't want to bother looking into collections in App.config, and it's safer if you explicitly force twelve properties, all of which are named. If one is invalid, at least you get the other input settings you specified.

Mapping keyboard input is really similar. In fact, I copy/pasted most of this code and find/replaced GamePad with Keyboard. That's just XNA being consistent. Isn't it great?

Integrating the InputMapper

Anyway, now that we have two concrete InputMapper implementations, time to figure out how to fit it into the existing code.

I started in the MainGame class, adding a field InputMapper input, and rewriting the Update method like so:

Next was initializing the input field, so I went back to the InputMapper class and added a static method CreateDefault to create a default instance according to the configuration.
To do that effectively, I had to add a boolean property PreferGamePad on the GameConfigurationSection class. If a game pad is preferred, we should try to return a GamePadMapper. If we can't responsibly do that, i.e. when no game pad is connected, we return a KeyboardMapper instead.

Finally, I added constructors to both concrete mappers, both of which use their respective configuration sections to fill the buttonMap[].

And that's pretty much it. Fairly simple code, the bulk of it is mostly boilerplate.
With that out of the way, however, we can turn back to talk of visual design and tile maps, and finally get some graphics on the screen. Hopefully.

No comments:

Post a Comment