(I originally posted this on my MSDN blog.)
Once I had the event aggregator set up in GenesisEngine I could think about how to turn keyboard and mouse input into events that other parts of the app could respond to.
The XNA framework doesn’t offer as much help in this area as you might be used to in Windows Forms or WPF. There isn’t any built-in windowing, or UI controls, or a commanding system so you pretty much have to build your own from scratch. This isn’t a terribly difficult task, I suppose, but I like the way mine turned out.
XnaInputState
All XNA offers for input is a KeyboardState and MouseState struct every update cycle that contain the current state of the keyboard (the current up or down state of every key) and the current state of the mouse cursor (where it currently is and whether each button is currently up or down).
In order to figure out interesting things like did was a certain key just now pressed this update or has it been held down for awhile, or how far did the mouse move between the last update and this one, you’ve got to track both the last state and the current one and check the differences yourself. The XnaInputState class handles this responsibility but it’s pretty trivial so I won’t list it here.
InputMapper
The InputMapper class is a bit more interesting. It stores mappings between states and event messages that should be sent when those states occur, where states in this case mean a key was pressed, or a key is being held down, or the mouse cursor moved. The mappings are set up in code right now but could be loaded from a config file in the future. This is from the main Genesis program class:
void SetInputBindings() { _inputMapper.AddKeyDownMessage(Keys.W); _inputMapper.AddKeyDownMessage(Keys.S); _inputMapper.AddKeyDownMessage(Keys.A); _inputMapper.AddKeyDownMessage(Keys.D); _inputMapper.AddKeyDownMessage(Keys.E); _inputMapper.AddKeyDownMessage(Keys.C); _inputMapper.AddKeyDownMessage(Keys.Z); _inputMapper.AddKeyPressMessage(Keys.F); _inputMapper.AddKeyPressMessage(Keys.U); _inputMapper.AddKeyPressMessage(Keys.P); _inputMapper.AddKeyPressMessage(Keys.OemPlus); _inputMapper.AddKeyPressMessage(Keys.OemMinus); _inputMapper.AddKeyPressMessage(Keys.G); _inputMapper.AddKeyPressMessage(Keys.Escape); // TODO: we don't specify which mouse button must be down // (hardcoded to right button ATM), // this can be extended when we need to. _inputMapper.AddMouseMoveMessage(); }
When a message and an input state are mapped together, InputMapper stores them in lists for later use. Specifically, it stores a set of delegates that will be executed when the correct input conditions are detected and these delegates send the proper messages to the event aggregator to be forwarded to whomever is interested in them.
I’m creating and storing a delegate that sends an event message rather than simply storing the type of the message because that was the only way I could figure out how to call EventAggregator.SendMessage with a strongly-typed message object. Essentially I have to capture a generic type parameter, save it away, and later pass it to another generic method without losing its type information. Creating a delegate at save time accomplishes that. I’m not thrilled with how obtuse it makes the code but it’s livable for now. I wouldn’t mind finding a better solution, though.
public void AddKeyPressMessage(Keys key) where T : InputMessage, new() { _keyPressEvents.Add(new KeyEvent { Key = key, Send = x => _eventAggregator.SendMessage(new T { InputState = x}) }); } public void AddKeyDownMessage(Keys key) where T : InputMessage, new() { _keyDownEvents.Add(new KeyEvent { Key = key, Send = x => _eventAggregator.SendMessage(new T { InputState = x }) }); } public void AddMouseMoveMessage() where T : InputMessage, new() { _mouseMoveEvents.Add(new MouseMoveEvent { Send = x => _eventAggregator.SendMessage(new T { InputState = x }) }); } private class KeyEvent { public Keys Key; public Action Send; } private class MouseMoveEvent { public Action Send; }
During each update, InputMapper is told to handle input and is passed an IInputState reference. Based on this input state, it finds any message-sending delegates who’s conditions match the current conditions and executes those delegates. InputMapper doesn’t know anything about who’s interested in input events, it just fires them.
public void HandleInput(IInputState inputState) { SendKeyPressMessages(inputState); SendKeyDownMessages(inputState); SendMouseMoveMessages(inputState); } private void SendKeyPressMessages(IInputState inputState) { foreach (var keyEvent in _keyPressEvents.Where(keyEvent => inputState.IsKeyPressed(keyEvent.Key))) { keyEvent.Send(inputState); } }
I like how the responsibilities are clearly separated in this system:
- Tracking input state changes – XnaInputState
- Firing events based on the current input state – InputMapper
- Actually sending the events to listeners – EventAggregator
- Receiving and acting on events – Implementers of IListener
I think it would be interesting to see how to incorporate the new Reactive Extensions into the input system. Rather than checking the current input state against a set of mappings every time, the InputMapper would set up some LINQ expressions against an input event sequence. I haven’t tried using the Reactive Extensions yet but from what I’ve read so far it seems like it should simplify the concepts here.