Hello everyone!
First time posting here on GD, although definitely not my first visit to these forums
Been studying C# (+ .NET framework) give or take 5 months now, done some basic PHP/HTML/Javascript in the past.
I figured, now that I know a lot more about OO design principles, and being a gamer at heart, I'd take a more serious look at game development.
So after recreating an ugly (codewise) but functional Pong in MonoGame, i thought I'd start looking at writing code I can recycle in future projects.
Decided to start with the inputs (keyboard, gamepads, mouse), seeing as it's a core part of any game, and a fairly general-purpose one at that.
I googled the topic for a few hours, and most people seemed to favour a 3-layer approach. But seeing as XNA/MonoGame takes care of all but the last layer I've focused on that.
So basicly, my motive behind this post is to get your feedback and ideas on how to improve / expand upon what I've come up with so far.
Everything from broader design ideas to small performance optimizations are of value and greatly appreciated. (I'm a sucker for smart "best practice" code)
So here's my InputHandler class:
// rough, unfinished input handler class
//
// primary focus (so far) has been:
// - learning C#, XNA/MonoGame and general design choices in game development. :)
// - centralized handling of all inputs.
// - event-driven top layer "abstraction"
// - reusable code.
//
// obviously there's lots of room for improvement, and a couple of ideas are:
// - custom collection class which incorporates all relevant bindings for a specific playerIndex.
// - expand upon above idea using inputs (Keyboard, GamePad, Mouse) as component objects referenced in a List
// and loop through this List in the Update() method.
// - adding more modularity & abstraction in general, perhaps allowing for actions/events being added dynamicly.
// i.e. making the handler a better fit for a general-purpose top layer game framework.
// - gamestate-aware/intelligent bindings, some configurable and some static.
class InputHandler
{
private KeyboardState _keyboardStateCurrent, _keyboardStatePrevious;
private Dictionary<InputActions, Keys> _keyboardBindings;
private GamePadState _gamePadStateCurrent, _gamePadStatePrevious;
private Dictionary<InputActions, Buttons> _gamePadBindings;
private List<PlayerIndex> _playerIndexes;
public List<PlayerIndex> PlayerIndexes
{
get { return _playerIndexes; }
}
public delegate void InputEvent();
// define events
// ("dead" default subscribers to avoid having to null check before invokes)
public event InputEvent onMoveLeft = delegate { };
public event InputEvent onMoveLeftHold = delegate { };
public event InputEvent onMoveLeftRelease = delegate { };
public event InputEvent onMoveRight = delegate { };
public event InputEvent onMoveRightHold = delegate { };
public event InputEvent onMoveRightRelease = delegate { };
public event InputEvent onJump = delegate { };
public event InputEvent onJumpHold = delegate { };
public event InputEvent onJumpRelease = delegate { };
public event InputEvent onNavigateBack = delegate { };
public InputHandler()
{
_keyboardStatePrevious = Keyboard.GetState();
_playerIndexes = new List<PlayerIndex>() { PlayerIndex.One };
_gamePadStatePrevious = GamePad.GetState(_playerIndexes[0]);
// if config file exist then load it here, else...
ResetBindings();
}
private void PollKeyboard()
{
_keyboardStateCurrent = Keyboard.GetState();
foreach (KeyValuePair<InputActions, Keys> keyBinding in _keyboardBindings)
{
// fire events when key pressed
if (_keyboardStateCurrent.IsKeyDown(keyBinding.Value) && _keyboardStatePrevious.IsKeyUp(keyBinding.Value))
{
switch (keyBinding.Key)
{
case InputActions.MovementLeft:
onMoveLeft();
break;
case InputActions.MovementRight:
onMoveRight();
break;
case InputActions.MovementJump:
onJump();
break;
case InputActions.NavigationBack:
onNavigateBack();
break;
}
}
// fire events key held
else if (_keyboardStateCurrent.IsKeyDown(keyBinding.Value) && _keyboardStatePrevious.IsKeyDown(keyBinding.Value))
{
switch (keyBinding.Key)
{
case InputActions.MovementLeft:
onMoveLeftHold();
break;
case InputActions.MovementRight:
onMoveRightHold();
break;
case InputActions.MovementJump:
onJumpHold();
break;
}
}
// fire events when key released
else if (_keyboardStateCurrent.IsKeyUp(keyBinding.Value) && _keyboardStatePrevious.IsKeyDown(keyBinding.Value))
{
switch (keyBinding.Key)
{
case InputActions.MovementLeft:
onMoveLeftRelease();
break;
case InputActions.MovementRight:
onMoveRightRelease();
break;
case InputActions.MovementJump:
onJumpRelease();
break;
}
}
}
_keyboardStatePrevious = _keyboardStateCurrent;
}
private void PollGamePad(PlayerIndex playerIndex)
{
_gamePadStateCurrent = GamePad.GetState(playerIndex);
foreach (KeyValuePair<InputActions, Buttons> buttonBinding in _gamePadBindings)
{
// fire events when button pressed
if (_gamePadStateCurrent.IsButtonDown(buttonBinding.Value) && _gamePadStatePrevious.IsButtonUp(buttonBinding.Value))
{
switch (buttonBinding.Key)
{
case InputActions.MovementLeft:
onMoveLeft();
break;
case InputActions.MovementRight:
onMoveRight();
break;
case InputActions.MovementJump:
onJump();
break;
case InputActions.NavigationBack:
onNavigateBack();
break;
}
}
// fire events when button held
else if (_gamePadStateCurrent.IsButtonDown(buttonBinding.Value) && _gamePadStatePrevious.IsButtonDown(buttonBinding.Value))
{
switch (buttonBinding.Key)
{
case InputActions.MovementLeft:
onMoveLeftHold();
break;
case InputActions.MovementRight:
onMoveRightHold();
break;
case InputActions.MovementJump:
onJumpHold();
break;
}
}
// fire events when button released
else if (_gamePadStateCurrent.IsButtonUp(buttonBinding.Value) && _gamePadStatePrevious.IsButtonDown(buttonBinding.Value))
{
switch (buttonBinding.Key)
{
case InputActions.MovementLeft:
onMoveLeftRelease();
break;
case InputActions.MovementRight:
onMoveRightRelease();
break;
case InputActions.MovementJump:
onJumpRelease();
break;
}
}
}
_gamePadStatePrevious = _gamePadStateCurrent;
}
// update method to call in gameloop
public void Update()
{
PollKeyboard();
foreach (PlayerIndex playerIndex in _playerIndexes)
{
if (GamePad.GetState(playerIndex).IsConnected)
{
PollGamePad(playerIndex);
}
}
}
// add plauyerIndex to list of gamepads to poll
public void AddPlayerIndex(PlayerIndex playerIndex)
{
if (!_playerIndexes.Contains(playerIndex))
{
_playerIndexes.Add(playerIndex);
}
}
// remove playerIndex from list of gamepads to poll
public void RemovePlayerIndex(PlayerIndex playerIndex)
{
if (_playerIndexes.Contains(playerIndex))
{
_playerIndexes.Remove(playerIndex);
}
}
// set default bindings
// (future: add more bindings obviously; hardcoded defaults or move them to a default config file?)
public void ResetBindings()
{
_keyboardBindings = new Dictionary<InputActions, Keys>() {
{ InputActions.MovementLeft, Keys.Left },
{ InputActions.MovementRight, Keys.Right },
{ InputActions.MovementJump, Keys.Space },
{ InputActions.NavigationBack, Keys.Escape }
};
_gamePadBindings = new Dictionary<InputActions, Buttons>() {
{ InputActions.MovementLeft, Buttons.DPadLeft },
{ InputActions.MovementRight, Buttons.DPadRight },
{ InputActions.MovementJump, Buttons.A },
{ InputActions.NavigationBack, Buttons.Y }
};
}
public void ChangeBinding(InputActions inputAction, Keys key)
{
if (_keyboardBindings.ContainsKey(inputAction))
{
_keyboardBindings[inputAction] = key;
}
}
public void ChangeBinding(InputActions inputAction, Buttons button)
{
if (_gamePadBindings.ContainsKey(inputAction))
{
_gamePadBindings[inputAction] = button;
}
}
// various enumerated actions
// (future: maybe seperate input actions for each gamestate into seperate enums?)
enum InputActions
{
MovementLeft,
MovementRight,
MovementJump,
NavigationBack
}
}
I'm using it by creating an instance of InputHandler in my Game1 class, calling InputHandler.Update() in the gameloop, passing it around and hooking up game entity movement methods to the InputHandler events.
Aside from the class itself, any ideas on how to improve the design in terms of where I should instanciate it?
Perhaps instanciate it in a StateHandler?
Or use seperate instances of InputHandler for each Player?
Or should I rather make the InputHandler a singleton? (I keep reading that singletons are evil )
Thanks in advance for taking the time to help a newbie out!
Cheers!
Edit:
Anyone know if there are any drawbacks to using events for inputs?
Does it scale well performancewise?
Do I need to worry about timing issues?
Any specific buffering techniques I could/should implement?