I'm looking for a way or general best practice advices for decoupling the architecture of my game, in the example below the input from the current game state workflow / entity behavior. While I'm all in for certain game development (or general) patterns such as an event- or component-driven design, I'm failing to understand how a decoupled architecture can work in a case where e.g. an entity actually needs to know a little bit more.
Take for example this little (bad) example:
// some component attached to an entity
void Update(long elapsedTime) {
var keyboardState = _keyboardDevice.State;
var gameState = _gameStateStack.CurrentState;
if(keyboardState.IsPressed(Keys.Enter) {
if(gameState.GetType == typeof(MenuState))
((MenuState)gameState).ExitMenu();
} else {
parent.Jump();
}
}
}
The requirements are pretty simple: If we're in the game menu, pressing enter closes the menu. However if the menu is not active, pressing enter causes the entity to jump. If I'm going to decouple the menu and the entity component using an event-driven approach...
// menu state
class MenuState : IHandle() {
void Handle(KeyCommand keyCommand) {
if(keyCommand.Key == Keys.Enter && keyCommand.IsPressed)
...
}
}
// entity component
class EntityControllerComponent : IHandle() {
...
}
... the concerns are clearly separated. But what happens now if I press the ENTER key? Who decides which part of the game is allowed to actually receive and handle the event?
Answer
Input is hard. Most of the simple patterns you see frequently in game dev just don't work well for input, at least at the low level.
Typically, for any kind of GUI, you need to have some concepts of focus and possibly also bubbling. The HTML/DOM model here is a good resource.
In such a setup, there is a sorted queue of event listeners. For a GUI, this would be a stack of widgets starting from where focus lies. For games, this might be the stack of game states or overlay panels. The input event is either only delivered to the top-most listener (so any overlay implicitly "blocks" all others from receiving input) or is handed to each in turn (giving each a chance to optionally block the remaining listeners from receiving the event).
There are weird complexities with input handling, too. For instance, in many input designs, you differentiate between KeyPressed
and KeyReleased
events. However, if you just deal with those naively, it becomes possible for one listener to receive the KeyPressed
for a particular key and a completely different one to receive the KeyReleased
. The first system might have some important logic that must be run after the release but it never finds out about it. You can see this in some games where you start moving, a dialog pops up, and the character continues moving even though you've let go of the movement key.
A solution for the above then is to remember which listener received any particular KeyPressed
or ButtonPressed
event and ensure that it receives the corresponding release events independent of which listener has priority. Likewise, if the whole app is unfocused (say via alt-tabbing away) then the input module can be sure to send simulated release events to any listeners that had received press events.
I can't stress enough: you basically have to use a pattern like that for handling the input at an application level.
Now, within the game itself, that is all inconvenient and wrong. You don't want your character controller to be dealing with input priorities and event canceling. In fact, you almost certainly don't want your character controller even handling low-level input events: the controller shouldn't have to figure out whether a key was Space
or RightArrow
. It just wants to know that the user pressed the JUMP key or that the right wants to move right.
This generally means you have an intermediate high-level gameplay input module. It registers into the input event priority list and then translates the low-level input events that it actually receives (individual key presses and the like) into generic gameplay-relevant events. It handles key mapping and rebinding, it handles abstraction gamepad input from keyboard input, and so on. The entire rest of the gameplay systems can then just deal with simple events while all the complexity of input focus is localized to that one gameplay input module.
TL;DR: use a priority list of input event listeners for low-level input and simple game events for high-level abstract gameplay controls.
No comments:
Post a Comment