State machines are one of the most common patterns in game programming. Chasm's atoms and enums make them clean and efficient.
Atom-Based State Machine
For simple cases, an atom attribute is all you need:
@state :: script = :menu
def on_tick(dt :: float) do
if @state == :menu do
update_menu(dt)
end
if @state == :playing do
update_game(dt)
end
if @state == :paused do
update_pause(dt)
end
if @state == :game_over do
update_game_over(dt)
end
end
defp update_menu(dt :: float) do
if key_pressed(:enter) do
@state = :playing
end
end
defp update_game(dt :: float) do
move_player(dt)
move_enemies(dt)
if @lives <= 0 do
@state = :game_over
end
if key_pressed(:escape) do
@state = :paused
end
end
defp update_pause(dt :: float) do
if key_pressed(:escape) do
@state = :playing
end
endEnum-Based State Machine
When the state carries data, use an enum:
enum GameState {
Menu,
Playing,
Cutscene,
GameOver
}
@gs :: script = GameState.Menu
def on_tick(dt :: float) do
case @gs do
when GameState.Menu -> update_menu(dt)
when GameState.Playing -> update_game(dt)
when GameState.GameOver -> update_game_over(dt)
_ -> 0
end
endcase here is used as an expression, each arm calls a function and returns its result (discarded with 0 in the wildcard arm). This reads cleanly and makes it obvious which function handles which state.
Enter/Exit Actions
Sometimes you need to run code exactly once when entering or leaving a state. Track the previous state:
@state :: script = :menu
@prev_state :: script = :none
defp transition(new_state :: atom) do
if @state != new_state do
on_exit(@state)
@prev_state = @state
@state = new_state
on_enter(new_state)
end
end
defp on_enter(state :: atom) do
if state == :playing do
@score = 0
@lives = 3
spawn_initial_enemies()
end
if state == :game_over do
if @score > @high_score do
@high_score = persist_copy(@score)
end
end
end
defp on_exit(state :: atom) do
if state == :playing do
@enemies.clear()
end
endPer-Entity State
Each entity can have its own state field:
defstruct Enemy do
x :: float = 0.0
y :: float = 0.0
hp :: int = 3
state :: atom = :patrol
timer :: float = 0.0
end
@enemies :: script = array_fixed(32, Enemy{})
defp update_enemy(e :: Enemy, dt :: float) :: Enemy do
if e.state == :patrol do
return patrol_step(e, dt)
end
if e.state == :chase do
return chase_step(e, dt)
end
if e.state == :dead do
return e
end
return e
end
def on_tick(dt :: float) do
i = 0
while i < @enemies.len do
e = @enemies.get(i)
@enemies.set(i, update_enemy(e, dt))
i = i + 1
end
endTimed Transitions
Use a timer field to automatically leave a state after a duration:
defstruct Player do
x :: float = 0.0
y :: float = 0.0
state :: atom = :normal
inv_timer :: float = 0.0 # invincibility frames timer
end
@player :: script = Player{}
defp take_hit(p :: Player) :: Player do
if p.state == :normal do
return p with { state: :invincible, inv_timer: 1.5 }
end
return p
end
def on_tick(dt :: float) do
p = @player
if p.state == :invincible do
p = p with { inv_timer: p.inv_timer - dt }
if p.inv_timer <= 0.0 do
p = p with { state: :normal, inv_timer: 0.0 }
end
end
@player = p
end