I’ve been working on a side project for a bit, which is a small game engine. I’m making it to learn how to build things from the ground up, and also to use it for my own game projects. A big part of that work right now has been focused on cleaning it up and abstracting things. This is necessary as it had gotten quite messy with me just trying to get stuff working without consolidating (which only works up to a certain point).
The part I’m trying to improve right now is my input system. Or rather, the fact that I had no central system for it at all. I was just querying the keyboard state directly and then had some if statements, which wasn’t exactly ideal.
With that in mind I started putting together a system. My requirements were that it had to:
- Be easy to remap input from one form to another
- Be able to parse input from a variety of sources (keyboard, controller, mouse, etc) into one output
- Support a callback system for sending out new input events
- Be easy to get input code into the parts of the game that actually did gameplay logic
I sourced heavily from this excellent article on GameDev.net. Most of the design is just that article expressed in my own way. To summarise my approach I’ll have to turn to the definitely-very-good MSPaint diagram I drew up for it.
Below you can see the actual code at its highest level. It consists of three distinct steps:
Getting Raw Input
Our first step will be to get the actual raw input from the hardware, this is what we’ll use as the base form of our input.
The Input Mapper queries for the raw input systems available through SDL every frame. It stores every distinct input it receives as a separate MappedInput object in a vector of inputs, with the raw input (specified as an enum), and any massaged values that it may need included in each entry.
An example of this would be when it detects mouse movement it will have the raw input being INPUT_MOUSE_MOTION, and the values being set to the relative movement of the mouse during that frame.
Mapping the Input
Once the raw input has been obtained then it’s time to map it from its raw form into in game actions that are abstracted away from specific keys.
This is done by looping over all of the Input Contexts that have currently been provided to the Input Mapper, passing the vector with all the raw input in it to each of them in turn. If a context returns true from their mapping function then that raw input is consumed, and it skips the remaining contexts to process the next raw input.
As you can see it’s a double loop, which I’d really like to find a way to get rid of, but nothing obvious has occurred to me. If you have any suggestions please let me know!
The context system is nice because it allows multiple control schemes to coexist. I don’t currently have a way to change the ordering for the contexts, but it’s definitely something I want to be able to do in future – so as to be able to put specific control schemes in pre-eminence at certain times. When I do that I’ll likely convert the vector to a LinkedList so the swapping around of elements has less cost.
An example of where this context system would be useful would be if you were making an FPS, and the character got into a vehicle. You’d want the vehicle movement actions to happen, but not the normal player characters. Maybe you’d also want them to be able to still shoot while in the vehicle, with this context system you can exert that kind of control.
Something I’ll support in the future is reading the mapping configurations from file. When that happens I’ll likely do away with specific C++ class implementations for different control schemes.
Calling the Callbacks
Okay! So our raw input has been gathered and mapped. Now we need is to get it out of the input mapping system and into our actual game code where we can do something with it.
For that we’ll be iterating over our mapped input, calling all of the input callbacks on each one of them. This is unfortunately another double loop, which once again if you can think of a way around I’d be happy to hear it.
The callbacks used here are all pointers to the base class for my input callbacks, which has derived classes for both member and free functions.
The way this is structured is that the callback objects themselves are not owned by the input system, but by whatever is calling it. The input system only holds pointers to ’em. This means that the calling code is responsible for subscribing and unsubscribing their callbacks from the input system. Failure to do so will result in an almost immediate crash as I do not check for null references when iterating over the callbacks.
As you can see above the actual subscription process is fairly easy. You just need to set up the callback object with SetArgs, and then subscribe to it via a handle to the input system.
Using the Input
We’ve got our mapped input, and now it’s been sent out to our game code. Now how can we use it once it gets there? The simple answer would be – any way you want! But here are a couple of examples:
Above you can see that you can use either the raw input that’s been received for this input, or the processed input that’s been mapped to an in game interaction. I’ve included the raw input as frequently it’s convenient for testing new functionality.
Plans for the Future
I’ve already mentioned some of my plans above – enabling the shuffling of input contexts for priority, as well as reading the mapping information in from file. Those are likely the first things I’ll tackle. Apart from cleaning up my code of course, I was more interested in getting a functioning system up and running, but now that it’s mostly there I’ll need to tidy it up.
There are so many interesting ways you can improve an input system, most of which I’m unlikely to do unless the whimsy strikes me. Things like gesture recognition and cross-frame delays (waiting more than a frame before recognising an input as valid). The reason for that is while in big game companies there are other peoples needs to consider – I don’t have to do that. What I’ve built is functional enough for me to play around with and get stuff done. After all, my main goal was just to tidy away my awful and messy direct querying for input code into something nicer, and that’s been accomplished.
I hope that this run through of my input system has been useful to you, please catch me on Twitter if you want to ask me anything about it.