5
\$\begingroup\$

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<KeyCommand>() {
    void Handle(KeyCommand keyCommand) {
        if(keyCommand.Key == Keys.Enter && keyCommand.IsPressed)
        ...
    }
}

// entity component
class EntityControllerComponent : IHandle<KeyCommand>() {
...
}

... 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?

\$\endgroup\$
  • 1
    \$\begingroup\$ In event drive design, two things: 1, if the menu is closed, it is not listening for the event. 2, if it is, it handles the event and cancels it, preventing it from propagating to the character (or rather, the character sees the event was canceled and does nothing). This requires that when the menu binds its event, it has priority (moved to the front of the queue) over the character's listener. \$\endgroup\$ – Draco18s Jan 13 '16 at 20:52
  • \$\begingroup\$ @Draco18s So first some sort of switchable event handlers as well as cancelable events. Got it. However for the ordering: Is there a general rule of thumb (first gui, then sound, then...), or just the order in which the handlers are being registered? \$\endgroup\$ – artganify Jan 13 '16 at 20:55
  • 1
    \$\begingroup\$ This question is extremely broad, but I have an answer to your specific example. If you make the question specific enough, I will answer explaining the solution web browsers use: Bubbling. \$\endgroup\$ – MickLH Jan 13 '16 at 21:10
  • \$\begingroup\$ You want a priority parameter passed to your event handler. Define 0 as lowest priority, with increasing integers as higher priority (or the reverse, it doesn't really matter, just declare it and then follow the declaration). When an event is registered, it gets inserted into an array that is sorted by priority. Same priority items are organized as "first in -> higher priority" (or lower, it really doesn't matter, two event callbacks with the same priority should not care which is called first: if they do, they shouldn't have the same priority!) \$\endgroup\$ – Draco18s Jan 13 '16 at 21:13
  • 1
    \$\begingroup\$ In this particular use-case you wouldn't want to use events in this way. Instead you'd want the game to know if there's a UI open and the character controller would ask the game, when it receives a keyboard event, "is any UI open?" and if so, not do anything. This is a more general approach and while it does couple the character controller to the main game loop, this is an acceptable binding. \$\endgroup\$ – Draco18s Jan 13 '16 at 21:28
10
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Accepted because it's the most comprehensive answer, taking other game aspects such as UI into account. \$\endgroup\$ – artganify Jan 14 '16 at 20:16
2
\$\begingroup\$

One possible way to decouple this example would be to use a stack of event handlers. Your program can iterate the stack in order, trying each listener until one handles the event:

class MenuState : public IKeyListener {
    ...
    boolean OnKeyEvent(const KeyEvent& keyCommand) {
        if (keyCommand.Key == Keys.Enter && keyCommand.IsPressed) {
            CloseMenu();
            return true;
        }

        // The engine calls the next IKeyListener
        return false;
    }
    ...
};

class PlayerState : public IKeyListener {
    ...
    boolean OnKeyEvent(const KeyEvent& keyCommand) {
        if (keyCommand.Key == Keys.Enter && keyCommand.IsPressed) {
            player.Jump();
            return true;
        }
        return false;
    }
    ...
};

But in order to retain flexibility in situations where multiple handlers may need to observe the event, "Bubbling" can be applied so that all handlers get to see the event:

class MenuState : public IKeyListener {
    ...
    boolean OnKeyEvent(const KeyEvent& keyCommand, boolean eventHandled) {
        if (eventHandled) return false;

        if (keyCommand.Key == Keys.Enter && keyCommand.IsPressed) {
            CloseMenu();
            return true;
        }

        // The engine calls the next IKeyListener
        return false;
    }
    ...
};

class PlayerState : public IKeyListener {
    ...
    boolean OnKeyEvent(const KeyEvent& keyCommand, boolean eventHandled) {
        if (eventHandled) return false;

        if (keyCommand.Key == Keys.Enter && keyCommand.IsPressed) {
            player.Jump();
            return true;
        }
        return false;
    }
    ...
};

class CheaterState : public IKeyListener {
    ...
    boolean OnKeyEvent(const KeyEvent& keyCommand, boolean eventHandled) {
        buffer += keyCommand.Key;
        if (buffer.Length > LongestCheat) {
            buffer = buffer.Substring(1);
        }
        if (TryCheatCode(buffer)) {
            buffer = "";
            return true;
        }
        return false;
    }
    ...
};
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.