State Pattern for Games
Created: 25.07.2016
The State Design Pattern for Simple Games
Often, games are characterized by several game states and different behavior, which is dependent on the current game state. Image, for example, the game Stratego that has two major phases:
- Placement of units (placement phase)
- Movement of units and fighting phase (move phase)
In short, the game Stratego is a two player board game typically played on a 10 x 10 field. There are different types of units that are placed secretly on the own half of the board in the placement phase. In the move phase the players alternately move their units on the board eventually meeting/attacking enemy units. Once two opposing units meet, a combat starts. The enemy unit is revealed and the combat is decided based on the type of units facing each other.
Source of image: CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=660417
For simplicity, we focus on the most important methods in both phases. The game field is omitted for the purpose of this article. In a minimalistic design as a monolithic block, the game might look like this:
In this architecture, the methods check for the current game state, because placing and removing units is only allowed during the placement phase. In addition, moveUnit() has different meanings depending on the current phase. While moveUnit() moves an already placed unit within the own half of the board in the placement phase, it moves or attacks with a unit in the move phase.
So the major points about this architecture are:
The logic of certain methods exclusively belongs to certain game states and/or certain methods have different meanings in different game states. The methods are all wired to a common object and thus state checks are required within the methods. The state checks are often performed with if/else-statements eventually increasing the complexity and the maintainability of the code.
Before applying the state pattern, you have to examine you game carefully and think about the number of different states, the number of methods in your game and if the methods behave differently in each state. Often, and this is not different with this pattern, applying design patterns comes with the price of architectural overhead. I recommend using this pattern if you face methods that behave differently in different game states. In this regard, the pattern helps a lot to produce understandable and maintainable code. In contrast, if you only have methods belonging exclusively to different game states, you might be better off with other approaches (see below).
Prerequisite: Representing or Deriving the Current Game State
Whether you apply the state pattern or not, the current state of the game needs to be computed to decide which methods are called and how they should behave. Let’s have a look at this requirement first. There are several ways to indicate the current game state, for example:
(1) Flags: After the players finished their placements a flag “isPlacementPhase” is set to false. The flag is used to compute the current state.
You are fine with this approach, if there are no more than two phases. Every possible game state can be derived by evaluating the flag. If the flag is true, it is the placement phase and if it is false, it must be the move phase. However, if there is a third phase, for example a game over phase, you need to extend your class with another flag: isPlacementPhase, isMovePhase, …
This approach is not easy to extend as each new phase requires a new flag and adjustments in existing code regarding the current phase calculation.
(2) Integer state variable or enumeration: An integer variable or an enumeration could be used to represent the phases. Both are easy to extend.
Personally, I think an enumeration is more elegant in this regard as it only accepts valid values.
Note: This way might be just fine in smaller games to capture the current game state, especially if all methods are wired exclusively to one game state. However, now back to our problem from above. Let’s assume, we use an enum to capture the game state. To ensure each method behaves correctly depending on the current game state, the code might look like this:
public void moveUnit(int fromX, int fromY, int toX, int toY){
if(gameState.equals(GameState.PLACE)){
//check if range is valid
//adjust position of unit
} else if(gameState.equals(GameState.MOVE){
//move the unit, eventually attack
} else if(gameState.equals(GameState.GAME_OVER){
//no moves are allowed
}
}
Each method requires if/else or switch statements to check the current state leading to the following flaws:
- Each state adds an additional case statement and increases the length of the corresponding method.
- You might be tempted to share or reuse code snippets, for example in the moveUnit() method, for similar behavior across states. However, this code-wiring might lead to side effects affecting parts of existing code whenever something changes in one state.
- It is hard to develop the Stratego class in a team as potentially everyone changes code in the three methods in the same class.
The state pattern can help to overcome these problems. The original pattern from the "gang of four" is depicted in the UML diagram below.
The game class receives an attribute of the abstract class GameState. Each possible state is implemented in a concrete subclass of GameState. There is exactly one concrete class active (set as the value of the attribute gameState) at a time to represent the actual state of the game.
As you can see the abstract class GameState contains all methods that were previously present in the game class. However, each state typically implements only a subset of these methods. Every method could be called at any time by refering to the abstract class GameState. However, depending on the currently active concrete implementation, only certain methods are available. The state ensures that the semantics of the method is correct and if not allowed methods are called, a proper error is thrown.
In general, the state pattern has the following advantages:
- Each state only contains relevant methods. Compared to the above-mentioned solution, the states are cleaner as the state-related code is in one place without large if/else statements.
- Methods with state-dependent semantics can be easily implemented in each state.
- Teams can independently develop the states as each state is in its own class.
- More states can be easily added.
For games, I prefer a customized version of this pattern using actions (also referred to as events in other contexts).
This version has some additional advantages:
- There is only one core method doAction() which takes a generic action as argument.
- As the class Action is abstract, you can easily add new actions, for example a SurrenderAction.
- Actions encapsulate all the necessary information to perform an action.
- The game can store an action history to eventually undo moves or replay the game.