Instead, it can help to formally define the distinct states in your game and the rules that determine which transitions between states should be allowed. These formal definitions are called state machines. Then, you can associate the relevant states with any code that determines what your game does on per-frame updates while in a particular state, when to transition to another state, and what secondary actions might result from the transition. By using a state machine to organize your code, you can more easily reason about complicated behaviors in your game.

When you start building a game, it’s easy to put all the state-dependent code in one place—for example, in the per-frame update method of a SpriteKit game. However, as your game grows and becomes more complex, that single method can become difficult to maintain or extend further.

In nearly all games, gameplay-related logic is highly dependent on the current state of the game. For example, animation code might change depending on whether a player character is currently walking, jumping, or standing. Enemy movement code might depend on high-level decisions made by your simulated intelligence for enemy characters, such as whether to chase a vulnerable player or flee from a strong one. Even which parts of your game should be running per-frame update code at any time might depend on whether the game is playing, paused, or in a menu or cut scene.

Before diving into the details of state machines in this chapter, you can get a quick taste of how they can simplify your game design by downloading the sample code project Dispenser: GameplayKit State Machine Basics . This simple game simulates a water dispenser, as shown in Figure 4-1. As the diagram displayed in the game shows, the dispenser can be in only one state at a time: empty, full, partially full, serving, or refilling. Using a state machine makes it easy to formalize and enforce this behavior, and also helps you organize sections of game logic that are specific to each state.

This sort of state machine design can be easily extended to cover many games. For example, additional Menu states could handle a complex series of menus, or a Cutscene state could run scene animations but not handle input.

GameOver. When entering this state, display a UI that summarizes the player’s actions and score, and respond to events for operating any interactive elements in that UI. Switch to the Playing state (starting a new game) upon appropriate user input.

Paused. Upon entering this state, apply a visual effect to the game screen to indicate that gameplay is paused. Remove the effect when exiting the state. (There is no need to suspend gameplay operation during this state, as game logic is invoked only during the Playing state.) Switch back to the Playing state upon appropriate user input.

Playing. While in this state, call per-frame update logic for gameplay. Switch to the Paused state upon appropriate user input.

TitleScreen. The app opens in this state. Upon entering this state, display a title screen. While in this state, animate the title screen. Switch to the Playing state upon user input.

In almost any game, a user interface depends on the state of the game, and vice versa. For example, pausing a game might show a menu screen, and while the game is paused, normal gameplay code should not take effect. You can control your game’s overall user interface with a set of states like those listed below and illustrated in Figure 4-4:

This state machine is further illustrated in the Building a Game with State Machines section below, as well as in the Maze sample code project.

Respawn. While in this state, simply track the amount of time spent since entering. After some time has elapsed, return to the Chase state.

Defeated. On entering this state, display an animation and increment the player’s score. While in this state, move what remains of the enemy to a central location where it will later reappear. Upon reaching that position, switch to the Respawn state.

Flee. On entering this state, display a vulnerable appearance. While in this state, change position on every frame to avoid the player. After some time has elapsed while in this state, return to the Chase state. If attacked by the player, switch to the Defeated state.

Chase. On entering this state, display the enemy’s normal appearance. While in this state, change position on every frame to pursue the player. If the player gains a power-up, switch to the Flee state.

Consider an arcade action game with enemy characters that pursue the player. The player can occasionally gain a power-up that makes the enemies temporarily vulnerable to attack, and defeated enemies are out of the game for some time before reappearing. Each enemy character can use its own state machine instance, with the following states, illustrated in Figure 4-3:

Falling. On entering this state, start a one-time falling animation. While in this state, move the character down a short distance (per frame). On reaching the ground, switch to the Running state and play a one-time landing animation and sound effect.

Jumping. On entering this state, play a sound effect, and start a one-time jumping animation. While in this state, move the character up a short distance (per frame), decelerating with gravity. When upward speed reaches zero, switch to the Falling state.

Running. While in this state, loop the run animation. If the player presses the jump button, switch to the Jump state. If the player runs off a ledge, switch to the Falling state.

Consider a 2D “endless running” game—in this style of game, the player character runs automatically, and the player must press a jump button to pass obstacles. Running and jumping have separate animations, and while running or jumping, the player’s position needs to be updated. This design can be expressed as a state machine with three states, illustrated in Figure 4-2.

State machines can be applied to any part of your game that involves state-dependent behavior. The following examples walk through designs of several different state machines.

Building a Game with State Machines

In GameplayKit, a state machine is an instance of the GKStateMachine class. For each state, you define the actions that occur while in that state, or when transitioning into or out of that state, by creating a custom subclass of GKState . At any one time, a state machine has exactly one current state. When you perform per-frame update logic for your game objects (for example, from within the update: method of a SpriteKit scene or the renderer:updateAtTime: method of a SceneKit render delegate), call the updateWithDeltaTime: method of the state machine, and it in turn calls the same method on its current state object. When your game logic requires a change in state, call the state machine’s enterState: method to choose a new state.

The Maze sample code project (already seen in the Entities and Components chapter) implements a variation on several classic arcade games. This game uses a separate instance of the state machine summarized above (see A State Machine for Enemy Behavior) to drive each of several enemy characters. Normally, enemies chase the player but flee when the player gains a power-up that allows them to be defeated. When defeated, they return to a respawn point, then reappear after a short time.

Note This section discusses features of the sample code project Maze: Getting Started with GameplayKit. Download it to follow along in Xcode.

Define States and Their Behavior Each state in the state machine is a subclass of GKState containing custom code that implements state-specific behavior. This state machine uses four state classes: AAPLEnemyChaseState , AAPLEnemyFleeState , AAPLEnemyDefeatedState , and AAPLEnemyRespawnState . All four state classes make use of general information about the game world, so all four inherit from an AAPLEnemyState class that defines properties and a common initializer used by all state classes in the game. Listing 4-1 summarizes the definitions of these classes. @interface AAPLEnemyState : GKState @property ( weak ) AAPLGame * game ; @property AAPLEntity * entity ; - ( instancetype ) initWithGame: ( AAPLGame * ) game entity: ( AAPLEntity * ) entity ; // ... @end @interface AAPLEnemyChaseState : AAPLEnemyState @end @interface AAPLEnemyFleeState : AAPLEnemyState @end @interface AAPLEnemyDefeatedState : AAPLEnemyState @property GKGridGraphNode * respawnPosition ; @end @interface AAPLEnemyRespawnState : AAPLEnemyState @end Most of these state classes need no additional public properties—all information they need about the game comes from their reference to the main AAPLGame object. This reference is weak, because the state objects are owned by state machines, which in turn are owned by entities, each of which is owned by the Game object. Each state class then defines state-specific behavior by overriding the GKState enter, exit, and update methods. For example, Listing 4-2 summarizes the implementation of the Flee state. - ( BOOL ) isValidNextState: ( Class __nonnull ) stateClass { return stateClass == [ AAPLEnemyChaseState class ] || stateClass == [ AAPLEnemyDefeatedState class ]; } - ( void ) didEnterWithPreviousState: ( __nullable GKState * ) previousState { AAPLSpriteComponent * component = ( AAPLSpriteComponent * )[ self . entity componentForClass: [ AAPLSpriteComponent class ]]; [ component useFleeAppearance ]; // Choose a target location to flee towards. // ... } - ( void ) updateWithDeltaTime: ( NSTimeInterval ) seconds { // If the enemy has reached its target, choose a new target. // ... // Flee towards the current target point. [ self startFollowingPath: [ self pathToNode: self . target ]]; } The state machine calls a state object’s didEnterWithPreviousState: method when that state becomes the machine’s current state. In the Flee state, this method uses the game’s SpriteComponent class to change the appearance of the enemy character. (See the Entities and Components chapter for discussion of this class.) This method also chooses a random location in the game level for the enemy to flee toward. The updateWithDeltaTime: method is called for every frame of animation (ultimately, from the update: method of the SpriteKit scene displaying the game). In this method, the Flee state continually recalculates a route to its target location and sets the enemy character moving along that route. (For a deeper discussion of this method’s implementation, see the Pathfinding chapter.) You can enforce preconditions or invariants in your state classes by having each class override the isValidNextState: method. In this game, the Respawn state is a valid next state only for the Defeated state—not the Chase or Flee state. Therefore, the code in the EnemyRespawnState class can safely assume that any side effects of the Defeated state have already occurred.