Menu
CoddyTech

State Pattern

Part of the Object Oriented Programming section of Coddy's GDScript journey. Lesson 54 of 65.

The State pattern gives an object a separate state object for each mode it can be in. The object hands its work to the current state, so its behavior changes when the state changes.

For a few states, an enum and a match are enough. When every state reacts to several actions in its own way, that match grows with every state and every action. The State pattern moves each state's rules into its own class:

# hero_state.gd
@abstract
class_name HeroState
extends RefCounted

@abstract func get_state_name() -> String
@abstract func handle(action: String) -> HeroState

handle() returns the next state: a new state object to switch to, or self to stay.

# idle_state.gd
class_name IdleState
extends HeroState

func get_state_name() -> String:
	return "Idle"

func handle(action: String) -> HeroState:
	if action == "run":
		return RunState.new()
	return self

RunState is written the same way: its name is "Running", and "stop" returns IdleState.new(). The hero, the context, keeps the current state and asks it what to do:

# hero.gd
class_name Hero
extends RefCounted

var state: HeroState = IdleState.new()

func handle_action(action: String) -> void:
	var next := state.handle(action)
	if next != state:
		print(state.get_state_name(), " -> ", next.get_state_name())
		state = next
# main.gd
var hero := Hero.new()
for action in ["stop", "run", "run", "stop"]:
	hero.handle_action(action)
Idle -> Running
Running -> Idle

The same action means different things in different states: "stop" does nothing while idle and stops a running hero. Each rule lives in the class of its state, so a new state, like jumping, is one new class.

The states return the next state instead of storing the hero. A state that kept the hero while the hero keeps the state would be a reference cycle, and neither object would ever be freed.

challenge icon

Challenge

Easy

A door is Locked, Closed or Open. DoorState (door_state.gd) declares get_state_name() and handle(action), and LockedState (locked_state.gd) is written: unlock makes the door Closed. Write:

  • ClosedState.handle(): open returns a new OpenState, lock a new LockedState, anything else self.
  • OpenState.handle(): close returns a new ClosedState, anything else self.
  • Door.handle_action(action): asks the current state for the next one. If it is the same object, print ACTION: still STATE; otherwise print ACTION: OLD -> NEW and switch to the new state.

The door starts Locked. The supplied code reads how many actions follow, then one action per line, and prints the final state. For open, unlock, open, lock the output is:

open: still Locked
unlock: Locked -> Closed
open: Closed -> Open
lock: still Open
Final state: Open

Your code goes in door.gd, closed_state.gd and open_state.gd. main.gd holds the supplied input/output code and cannot be edited.

Try it yourself

extends Node

func _ready():
	# Supplied input/output code: keep it as it is
	var door := Door.new()
	var count := int(OS.read_string_from_stdin())
	for i in count:
		door.handle_action(OS.read_string_from_stdin())
	print("Final state: ", door.state.get_state_name())
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online GDScript compiler