PVG
11. Finite State Machine — Program Video Games

11. Finite State Machine

[[programvideogames]]A Finite State Machine or FSM for short is an abstract machine that can be in only one of it's states at once.

To explain using our game as a reference point:

Our player's movement is only in one of these states at current:

  • Uncontrollable (respawning after hitting spikes)
  • Idle (awaiting input)
  • Running
  • Jumping
  • Falling

We can't be both Jumping and Running, nor Idle and Falling.

A finite state machine is a great fit for character controller code so that we don't end up checking a bunch of boolean values all the time. It gets confusing fast.

First thing's first, I have attached a new player sprite that has a 3rd row - our Run animation.

![[Pasted image 20240919163515.png]]

We can set it up under our other animations:

player_anim_run := Animation {
    size   = {120, 80},
    offset = {52, 42},
    start  = 0,
    end    = 9,
    row    = 2,
    time   = 0.15,
}

Don't forget to add it to the loading code:

    // creating player
    animations = {
        "idle" = player_anim_idle,
        "jump" = player_anim_jump,
        "jump_fall_inbetween" = player_anim_jump_fall_inbetween,
        "fall" = player_anim_fall,
        "run" = player_anim_run,
    },

Next thing, we'll create a new file to hold our player logic: src/player.odin

We'll create an enum to represent the player's movement state:

package main

import rl "vendor:raylib" // Will use this below

Player_Movement_State :: enum {
    Uncontrollable,
    Idle,
    Run,
    Jump,
    Fall,
}

Next, we'll add to our Game_State an instance of this, while deleting player_uncontrollable since it's represented in the enum:


Game_State :: struct {
    camera:                rl.Camera2D,
    entities:              [dynamic]Entity,
    solid_tiles:           [dynamic]Rect,
    spikes:                map[Entity_Id]Direction,
    debug_shapes:          [dynamic]Debug_Shape,
    safe_position:         Vec2,
    safe_reset_timer:      f32,
    player_movement_state: Player_Movement_State,
    // DELETED player_uncontrollable
    player_id:             Entity_Id,
}

This will break some code, so let's fix it:

In spike_on_enter, use the new enum and add a call to a proc we are going to make soon switch_animation:

    if other_id == gs.player_id {
        other.x = gs.safe_position.x
        other.y = gs.safe_position.y
        other.vel = 0
        gs.safe_reset_timer = PLAYER_SAFE_RESET_TIME
        gs.player_movement_state = .Uncontrollable
        switch_animation(other, "idle")
    }

Let's create that switch_animation proc in src/entity.odin

switch_animation :: proc(entity: ^Entity, name: string) {
    entity.current_anim_name = name
    anim := entity.animations[name]
    entity.animation_timer = anim.time
    entity.current_anim_frame = anim.start
}

In our main proc, we have a bunch of player and input related code at the top.

I'll show you what the top of main should look like:

    for !rl.WindowShouldClose() {
        dt := rl.GetFrameTime()

        // Make sure to get a new pointer every frame in case the entities
        // array is moved in memory
        player := entity_get(gs.player_id)

        player_update(&gs, dt)
        entity_update(gs.entities[:], dt)
        physics_update(gs.entities[:], gs.solid_tiles[:], dt)
        behavior_update(gs.entities[:], gs.solid_tiles[:], dt)

// ...

Where is this code?

We've copied it to player_update in src/player.odin and modified it.

Let's have a look:

player_update :: proc(gs: ^Game_State, dt: f32) {
    player := entity_get(gs.player_id)

    input_x: f32
    if rl.IsKeyDown(.T) do input_x += 1
    if rl.IsKeyDown(.R) do input_x -= 1

    player.vel.x = input_x * player.move_speed

    switch gs.player_movement_state {
    case .Uncontrollable:
        gs.safe_reset_timer -= dt
        player.vel.x = 0
        player.vel.y = 0
        if gs.safe_reset_timer <= 0 {
            switch_animation(player, "idle")
            gs.player_movement_state = .Idle
        }
    case .Idle:
        try_run(gs, player)
        try_jump(gs, player)
    case .Run:
        if input_x == 0 {
            gs.player_movement_state = .Idle
            switch_animation(player, "idle")
        }
        try_jump(gs, player)
    case .Jump:
        if player.vel.y >= 0 {
            gs.player_movement_state = .Fall
            player.current_anim_name = "fall"
            switch_animation(player, "fall")
        }
    case .Fall:
        if .Grounded in player.flags {
            gs.player_movement_state = .Idle
            switch_animation(player, "idle")
        }
    }
}

try_run :: proc(gs: ^Game_State, player: ^Entity) {
    if player.vel.x != 0 && .Grounded in player.flags {
        switch_animation(player, "run")
        gs.player_movement_state = .Run
    }
}

try_jump :: proc(gs: ^Game_State, player: ^Entity) {
    if rl.IsKeyPressed(.SPACE) && .Grounded in player.flags {
        player.vel.y = -player.jump_force
        player.flags -= {.Grounded}
        switch_animation(player, "jump")
        gs.player_movement_state = .Jump
    }
}

That's it for this one.

I hope you can see the usefulness of state machines in code organisation.