100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Building a Simple State Machine in Lua

A hands-on walkthrough of implementing a finite state machine in Lua using tables and functions, with a traffic-light example.

PracticeIntermediate10 min readJul 10, 2026
Analogies

What a State Machine Solves

A finite state machine (FSM) models a system that can only be in one of a fixed set of named states at a time, moving between states only via explicitly allowed transitions -- for example, a traffic light cycling red -> green -> yellow -> red, or a game character moving between idle -> walking -> jumping -> falling. Implementing an FSM explicitly, rather than scattering boolean flags and nested if statements throughout your code, makes illegal states unrepresentable and gives you one obvious place to add logging, validation, or enter/exit side effects whenever the state changes.

🏏

Cricket analogy: A finite state machine is like the fixed sequence of legal match states in cricket -- an innings can be 'in progress', 'declared', or 'all out', but never both 'declared' and 'all out' simultaneously -- and the umpire enforces which transitions are even legal to call next.

Modeling States and Transitions with Tables

The natural Lua representation of an FSM is a table of states, where each state is itself a table describing its allowed transitions and optional onEnter/onExit callback functions. For the classic traffic-light example, you'd define states.red = { transitions = { next = 'green' }, onEnter = function() print('Stop') end }, states.green = { transitions = { next = 'yellow' }, ... }, and so on, so that looking up states[currentState].transitions.next tells you exactly which state is legal to move to next without any scattered if chains.

🏏

Cricket analogy: Modeling states as a table of tables is like a scorer's rulebook that lists, for each match state, exactly which next state is legal -- from 'innings break' the only legal next entry is 'second innings starts', never 'match abandoned' unless a specific condition triggers it.

lua
local states = {
  red = {
    onEnter = function() print("STOP") end,
    transitions = { next = "green" },
  },
  green = {
    onEnter = function() print("GO") end,
    transitions = { next = "yellow" },
  },
  yellow = {
    onEnter = function() print("SLOW DOWN") end,
    transitions = { next = "red" },
  },
}

Implementing the State Machine Object

Wrap the states table in a small object using Lua's metatable-based OOP pattern: a constructor function creates an instance table holding current (the current state name) and a reference to the shared states definition, and a machine:transition(event) method looks up states[machine.current].transitions[event], calls the outgoing state's onExit (if defined), updates machine.current, and calls the new state's onEnter. This keeps all the state-change logic -- validation, exit hooks, entry hooks -- in exactly one method instead of duplicated across every place the code changes state.

🏏

Cricket analogy: The transition method centralizing all state-change logic is like a single third umpire review process that every dismissal appeal must go through, rather than each fielder deciding independently whether a batsman is out.

lua
local StateMachine = {}
StateMachine.__index = StateMachine

function StateMachine.new(states, initial)
  local self = setmetatable({}, StateMachine)
  self.states = states
  self.current = initial
  return self
end

function StateMachine:transition(event)
  local stateDef = self.states[self.current]
  local nextState = stateDef.transitions[event]
  if not nextState then
    return false, string.format("no transition '%s' from state '%s'", event, self.current)
  end

  if stateDef.onExit then stateDef.onExit() end
  self.current = nextState
  local newStateDef = self.states[nextState]
  if newStateDef.onEnter then newStateDef.onEnter() end
  return true
end

-- usage
local light = StateMachine.new(states, "red")
light:transition("next")  --> prints "GO", current = "green"
light:transition("next")  --> prints "SLOW DOWN", current = "yellow"
local ok, err = light:transition("brake")  --> false, "no transition 'brake' from state 'yellow'"

Handling Invalid Transitions and Side Effects

Deciding what happens on an invalid transition is a real design choice: the example above returns false, errorMessage so the caller can decide how to react (log it, ignore it, or raise a hard error with assert), which is safer than silently doing nothing, because a silently-ignored invalid transition can leave calling code believing a state change happened when it didn't. onEnter/onExit callbacks are the right place for side effects tied to a state itself -- like turning traffic-light bulbs on and off, or starting/stopping a game character's animation -- while side effects tied to the transition itself (like playing a one-off sound effect) belong in the transition call site, not baked into the state definitions.

🏏

Cricket analogy: Returning false on an invalid transition instead of silently ignoring it is like a third umpire clearly signaling 'not out' on the big screen rather than saying nothing, because an ambiguous non-signal would leave both teams unsure whether the review even happened.

A common mistake is mutating self.current before verifying the transition is valid, or before running onExit for the outgoing state -- always validate first, run onExit for the old state, only then update current, and finally run onEnter for the new state, so a rejected transition never leaves the machine in an inconsistent state.

Extending the Pattern: Hierarchical and Event-Driven States

Once the basic pattern works, two common extensions are worth knowing: hierarchical states, where a state like combat has its own nested sub-machine for aiming/reloading/meleeing so you don't have to enumerate every combination as a flat state; and coroutine-based state machines, where each state is a Lua coroutine that yields control back to the driver and resumes exactly where it left off, which is a natural fit for scripted sequences (cutscenes, tutorials) that need to pause mid-state waiting for an external event like a player input or a timer.

🏏

Cricket analogy: A hierarchical state machine is like a match's overall status of 'in progress' containing its own nested sub-states for the current over -- 'run-up', 'delivery', 'follow-through' -- rather than flattening every combination of match phase and ball phase into one giant list.

Lua's coroutine.wrap/coroutine.create are a genuinely elegant alternative to callback-table state machines for linear, scripted sequences, since the coroutine's own local variables and yield points naturally encode 'where we are' without an explicit states table at all.

  • A finite state machine restricts a system to one named state at a time, moving only via explicitly allowed transitions.
  • Represent states as a table of tables, each with its own transitions map and optional onEnter/onExit callbacks.
  • Centralize all state-change logic in a single transition(event) method rather than duplicating checks across the codebase.
  • Validate a transition before mutating current, running onExit for the old state and onEnter for the new one in order.
  • Return false, errorMessage (or raise via assert) on an invalid transition instead of silently doing nothing.
  • Hierarchical sub-machines avoid enumerating every state combination as a flat list.
  • Coroutines offer a lightweight alternative for linear, scripted sequences that pause and resume around external events.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#LuaStudyNotes#BuildingASimpleStateMachineInLua#Building#Simple#State#Machine#StudyNotes#SkillVeris#ExamPrep