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.