Command pattern for a Javascript / Typescript game
In this tutorial we are going to pause on writing code so that we can study the command pattern. Often in software development the same problem crops up again and again and eventually an agreed upon best practice design pattern
emerges for solving the problem. The command pattern is a generic software programming design pattern to help decouple the configuration of some service from the way certain actions or events are triggered when calling it.
The Gang of Four are the authors of the book, "Design Patterns: Elements of Reusable Object-Oriented Software" who are known for helping define and popularise some of the most commonly used software design patterns today including the command design pattern. You can read more about the gang of four design patterns here at the gang of four pattern website.
The command design pattern has gained popularity in the game development community because of it's problem overlap with handling game input and the simple solution it provides. We will be implementing our own version of the game command design pattern. For our purposes we will simplify the pattern and go over how we can apply it to creating our generic input handling framework below.
Game input handling and updating game state
Below is a high level example of how a game handles input and interprets that input correctly in order to update the game state.
Lets go over the main phases below.
Listen to operating system low level input events
In this phase we need to listen and capture all the low level input events for all the devices our game system supports. The term low level event
just means events that are closer to the hardware and operating system or in our case the browser, think html key press event, mouse button event, gamepad button tap, joystick etc. How a browser listens for and handles keyboad events is going to be distinct from how it handles mouse or gamepad events.
Transform low level input events into general high level action events
In this phase the low level events are read and transformed into generalised game action events for each player. In order to achieve this the individual player control input mappings are required.
In this phase we need an understanding of the different low level events and how they map to the core supported game actions, think horizontal (x), vertical (y) direction to represent a given inputs direction axis or primary and secondary actions. It can translate and combine all the low level input events into a standard input that represents high level
game actions. After this phase we no longer care if the f
key is pressed but rather that the primaryAction
is enabled.
Calculate game entities new state
In this phase we take the standard game action input and apply it to all the player game entities. Here player
refers to any game entity that is directly controlled by an input. Perhaps when the primary action
is enabled the player should attempt
to jump. Note the attempt
keyword is there on purpose as there could be situations where the player is unable to jump. For example if they are in mid air (and the game doesn't allow mid air jumping) or if they have run out of their jump ability or are currently taking damage. The point here is that all these conditions of when a player can and can't jump are unique to the game we are creating.
The actions may even be different depending on the state of the game, ie the primary action
could trigger a different event depending on if the game is on the inventory screen vs the interactive play state for an RPG game as an example. In this phase we just need to trigger events to attempt the required actions and let the next GameState resolution phase
take care of the actual effects on the game state.
Gamestate resolution
Based on the new game entities proposed state we may have produced an invalid game state. In this phase we must check if the game state is valid and use game logic to resolve any invalid game state. Examples of invalid game state is an object's position is out of bounds or an object is overlapping with another object when it should collide instead.
The game resolution logic is as simple or as complicated as it needs to be to adhere to the game rules.
Update game state
In this phase we update the game state based on the resulting resolved valid game state in the previous phase.
Game input command design pattern
In the above diagram the green boxes represent the phases that can be modelled with the help of the command design pattern. Looking at these phases in more detail will help in understanding the next tutorial where we will implement this pattern.
The first two phases have been expanded in the diagram below.
The left of the diagram shows the different input devices connected to the compatible and provided input ports. The devices emmit low level input events through the port which are mapped to the device specific input API state. The InputStateManager
listens to the various low level input events and captures these events in memory. This happens progressively, as more low level events occur inbetween game frames the input is updated to accomodate the new events.
The InputManager
retrieves the low level input events as well as each player's individual player input mappings. Then based on each invidual players selected input port and input control mappings the input manager is able to resolve each player's StandardGameInput
(high level input actions). We'll talk more about exactly what this StandardGameInput
looks like in the next tutorial. For now you can think of it as a representation of actions the game supports that are decoupled from a specific input device.
Each player now has an associated StandardGameInput
and these are used to calculate the new players state during the rest of the game input handling process phases mentioned above.
Summary
Hopefully from the explanations above you are starting to see how and why it is important to be able to decouple and abstract the low level hardware / operating system / browser input events from the high level game logic actions. By doing this we are making it easier for our game to not be limited to one input device and we are also defining a standard game action representation that is distinct from the input devices it supports.
Later in the course we'll see that the effort required to do this will be justified by how much easier it will be to extend our input system to support gamepad controllers.
Indeed once we have our game input framework in place we will be able to reuse it for many different games and shift our focus to what the different StandardGameInput
's mean in relation to the context and state of the unique game.
We'll start modelling some of these ideas in the next tutorial!