Animation control is a combination of two major topics. In the early stage, I roughly put the animation and control system architecture together, but it is still far from complete. At the moment I am focusing on the control side, so the following notes will be centered on control first.
This article mainly records two design issues around character movement and how I solved them with an architecture design. The first issue is that the character sometimes gets stuck when turning quickly. The second issue appears after the first one is fixed: there is a noticeable delay when the character changes direction.
For character movement, I use horizontal movement as the example. When the D key is pressed, the character moves to the right; when the A key is pressed, the character moves to the left. The first problem happens when the character turns direction and gets stuck. For example, if we hold D, the character keeps moving right; then if we quickly release D and press A, the character should move left. Our default input sequence looks like this:
D key held
(character moves right) -> D key released (character idle) ->
A key pressed (character moves left) -> A key held (character moves left)
The reason the character gets stuck while turning is that we often end up inputting something like this instead:
D key held (character moves right)
-> A key pressed (direction conflict) -> D key released (character idle) ->
A key held (character moves left)
It is obvious that a conflict and an idle state appear in the middle, which makes the character appear frozen for a short moment before continuing in the intended direction.
Another issue is that SDL also has a small delay before it starts receiving the repeated key-hold event, which makes the problem even more obvious.
The second issue is a turning delay after the first problem has been fixed, as shown in the animation below:
The first step to solving the issue is that we cannot rely on Keyboard Events received in each animation frame loop as the basis for action changes. Instead, we record the state of each key and use that state for decision making. The state can be stored as:
std::unordered_map<key, bool>
Each key is represented by a boolean to indicate whether it is currently pressed, and then the event handler accesses this structure to decide how the character should move.
However, the architecture still has a fatal issue: the event receiver and the command executor should not be tightly coupled. In other words, the event handler should not wait until an event arrives before deciding whether to execute a command. Instead, it should evaluate command execution in every animation frame loop. A simplified description is shown below:
while (true) { // every animation frame loop
poll_event() // poll events -> decide whether to execute commands
...
}
while (true) {
poll_event() // poll events
handler->execute() // whether new events arrive or not, we still evaluate command execution
}
In summary, I use the following UML to describe my architecture design:
There are three main roles: Handlers, Keyboard Event Handler, and Event. Handlers are the top-level event receiver. They classify the received events and dispatch them to individual handlers below. Note that the name ends with an s, which indicates a manager of multiple handlers. Since events are not limited to keyboard events and can also include window events, there may be other handler types in the future.
Handlers and Events are bidirectionally associated. Handlers follow the Command Pattern and manage the events (or processes) that need to be executed when certain conditions are met. Each Event also stores the handler pointer that executes itself, forming a two-way association.
The implementation result is shown in the animation below:
Last updated: