State of Play

Lecture W5T1: State Machines, and the Lab 3 Launch

Lucas P. Cordova, Ph.D.

Willamette University

September 22, 2026

Insert Coin

Today

LECTURE W5T1 // STATE OF PLAY

  1. Sprint pulse (2 minutes): how is your team’s repo?
  2. Meet the rival (live demo): a chase with no brain in it.
  3. The pattern: state machines, the brains behind every controller.
  4. The rival gets a brain (live): an enum, a match, and exactly one door.
  5. Lab 3 launches: the same pattern, turned sideways into a platformer.
  6. Studio: start building with me in the room.

Housekeeping: Lab 3 is due Thursday, October 1, 9:59 PM. The team repo link assignment is due today; if your team has not submitted, do it during studio.

Sprint pulse

2 minutes with your teammate:

  1. Is the team repo up, with the prototype photo committed?
  2. Have you refined the idea since Thursday? One sentence on what changed.
  3. One thing you want to ask me during studio.

Meet the Rival

A new arena

Today’s demos live in a new repo, rival-brains: one arena, Blitz, and one rival. Follow along or just watch; every demo is a branch you can check out later.

  1. Clone rival-brains and open project.godot in Godot.
  2. git checkout demo1, then run the project.
  3. Move with arrows or WASD. Try to lose the ghost.

You cannot lose the ghost. It never rests, never patrols, never gives up. It has no moods, because it has no brain. Just one urge, forever.

The rival’s whole brain

On branch demo1, in rival.gd. This is all of it:

extends CharacterBody2D

@export var speed: float = 180.0

@onready var player: Node2D = get_node("../Player")

func _physics_process(_delta: float) -> void:
    var to_player := player.global_position - global_position
    velocity = to_player.normalized() * speed
    move_and_slide()

Ten lines, and it works. It is also done growing. Watch what happens when we ask for more.

Feature request: everything

  • Patrol a route until Blitz gets close? Add var is_patrolling := true and an if around the chase.
  • Give up when Blitz gets far away? Another distance check, wired against the first one.
  • Frightened when Blitz grabs a fig? Add var is_frightened := false and var fear_timer := 0.0, and now the patrol if needs and not is_frightened, and so does the chase…
  • Every new mood interrogates every old flag. The bug that ships: frightened while un-patrolled at exactly the give-up distance does something nobody designed, because nobody designed it.

The disease has a name

Implicit state. Your booleans are states, just unnamed and unguarded. Eight flags means 256 possible combinations, and you designed maybe six of them. The other 250 are bugs waiting for a playtester to find.

The cure is to stop letting states happen by accident and start declaring them.

The Pattern

State machines

A finite state machine is a brain with rooms: a fixed set of named states; the machine is in exactly one at a time; each state owns its own rules (what happens here) and its own exits (when and where to leave); and every state change is an explicit, deliberate event.

This is GPP: State, this week’s reading, and it is the oldest reliable brain in games.

You have been playing them forever

  • Pac-Man’s ghosts: chase, scatter, frightened. Our four rivals will run exactly this, and the Fig Break is nothing but a forced transition into frightened.
  • Mario: small, super, fire. The states even wear different sprites.
  • Every game you have ever booted: menu, playing, paused, game over. Screens are states too (Thursday makes that literal).
  • And Blitz’s four class years? First-Year through Senior are states with different sizes and rules. Growth is a transition.

The transition table

A state machine fits on an index card. The rival we want:

From When To
PATROL Blitz closer than the sight range CHASE
CHASE Blitz farther than the give-up range PATROL

The two separate thresholds are deliberate: sight at 260, give-up at 360. With a single threshold the rival would flicker between moods right at the line. The gap is called hysteresis, and every good chase AI has it.

If you cannot draw this table, the brain is still flag soup. Sketch one for your project’s enemies too.

The Rival Gets a Brain

Name the rooms

On branch demo2, rival.gd starts with two lines that change everything:

enum State { PATROL, CHASE }

var state: State = State.PATROL
  • An enum is a set of named values. Under the hood they are integers; in your code they are rooms with names on the doors.
  • One variable holds the current room. The rival is never in two rooms, and never in none.

Each room, its own rules

The heart of rival.gd on demo2:

    match state:
        State.PATROL:
            var target := patrol_points[patrol_index]
            if global_position.distance_to(target) < 12.0:
                patrol_index = (patrol_index + 1) % patrol_points.size()
                target = patrol_points[patrol_index]
            velocity = (target - global_position).normalized() * patrol_speed
            if to_player.length() < sight_range:
                _change_state(State.CHASE)
        State.CHASE:
            velocity = to_player.normalized() * chase_speed
            if to_player.length() > give_up_range:
                _change_state(State.PATROL)

Each room owns its movement and its exits, and nothing else. PATROL knows nothing about chasing. CHASE has never heard of waypoints.

Exactly one door

Every transition goes through one function, in rival.gd:

func _change_state(new_state: State) -> void:
    state = new_state
    print("rival: ", State.keys()[new_state])
    match new_state:
        State.PATROL:
            modulate = Color.WHITE
        State.CHASE:
            modulate = Color("ff2e97")
  • The door announces (the print) and dresses (the tint). When you enter a room, the room dresses you.
  • Sounds, sprite swaps, particle bursts: every “when I become X” effect lives in this one function.

Run it

  1. git checkout demo2 (accept Godot’s reload prompt; settings never change between branches).
  2. Run. The rival walks its route in white, flashes magenta the moment you cross the sight range, and gives up back to white when you outrun it.
  3. Watch the Output panel narrate every mood swing.

Same ghost, ten more minutes of work, an actual personality. Rules live in rooms; effects live in the door; the table we drew is exactly the code we wrote.

Features live in rooms now

  • Want FRIGHTENED? It is a new room plus one knock on the door from outside. Nobody rewrites PATROL or CHASE. We build it live on Thursday (branch demo3), fig and all.
  • Want the game itself to have title, playing, and caught screens? That is a machine too (demo4, also Thursday).
  • And in your Lab 3, the rooms are IDLE, RUN, AIR, and the AIR room simply has no jump line in it. Features by omission beat features by flag.

Lab 3 Launch

Your third game

Lab 3: Platformer. The rival’s brain pattern, turned sideways: a Bearcat who runs, jumps, dies on spikes, and plants a flag. You get the level geometry and three Bearcat poses free. You build the body, then you build the brain.

Milestone What exists after it
M1 A body with gravity: run, jump from the floor
M2 The brain transplant: enum + match + one door, same behavior
M3 States you can see: each state wears its own pose
M4 The world pushes back: spikes respawn you, the flag declares CLEAR
M5 Make it yours: at least one twist, and the best twists are new states

The contract

Due Thursday, October 1, 9:59 PM. Push to main at every milestone; pushing is the submission. The README’s requirements are the contract: custom actions, an explicit state machine with one transition function, visible states, a world that pushes back, named layers with a written layer map, one custom feature, class code style.

The M5 menu leans into new states: a dash, a wall slide, coyote time. That is deliberate. The lab is not really about platforms; it is about rooms and doors, and your project’s controllers are next.

Studio: press start

Rest of class. In order:

  1. Accept the Lab 3 assignment (link on Canvas) and clone your repo.
  2. Open GUIDE.md and build M1. Aim to have a Bearcat jumping on platforms before you leave this room.
  3. Stuck? The Nothing happened? table at the bottom of the guide, then a neighbor, then me. I am right here.

Game Over Screen

What you know now

  • Flag soup is implicit state: unnamed, unguarded, combinatorial, and where AI bugs come from. You watched demo1 hit that wall.
  • A state machine declares the rooms: one active state, each owning its rules and exits, transitions explicit, drawn first as a table.
  • In GDScript that is an enum, a match, and one _change_state door that owns every entry effect (demo2’s tint included).
  • The same pattern powers your Lab 3 Bearcat, Thursday’s frightened rival, and eventually all four rivals in Pac-Blitz.

Quest log

Lab 3: Platformer is due Thursday, October 1, 9:59 PM: pushed to main, Your Submission section complete.

Team repo link is due today if your team has not submitted it yet.

Reading (highly recommended, short): GPP: State. You now have the scars to appreciate it.

Thursday: the rival’s brain grows. The fig forces FRIGHTENED from outside (demo3), the game itself becomes a machine with title and game-over screens (demo4), and your teams get sprint time. The proposal gate (Thursday, October 8) is on the horizon.

Schedule update

The lab calendar has shifted to protect your project runway. The syllabus is updated (v1.0), and the Canvas home page matches:

  • Lab 3 (platformer): due Thursday, October 1.
  • Lab 4 (component kit): assigned Thursday, October 1; due Tuesday, October 13.
  • Lab 5 (UI + game feel): due Tuesday, October 27.
  • Project gates unchanged: proposal October 8, prototype October 15.

References

Sources

  1. Nystrom, Game Programming Patterns: State.
  2. The rival-brains demo repo: branches demo1 through demo4, one brain upgrade each.
  3. Lab 3 starter repo: GUIDE.md, milestones M1 through M5.