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.
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.
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
1. What is the main benefit of implementing an explicit finite state machine instead of scattered boolean flags?
2. In the traffic-light example, what does states[currentState].transitions.next represent?
3. In the example StateMachine:transition implementation, in what order do the operations happen for a valid transition?
4. What is a key advantage of a coroutine-based state machine for scripted sequences?
5. Why should an invalid transition attempt return false and an error message rather than silently doing nothing?
Was this page helpful?
You May Also Like
Lua Best Practices
Practical conventions and patterns for writing clean, efficient, and maintainable Lua code.
Lua Quick Reference
A condensed cheat sheet of core Lua syntax, standard library functions, and idioms for quick lookup.
Lua Interview Questions
Common Lua interview questions and answers covering tables, metatables, scoping, closures, and coroutines.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics