Hello again! Welcome back to our course on FSMs.
In our last lesson, you successfully built your first finite state machine in Luau using if-elseif logic. You saw how this approach directly translates a state diagram into code, but you also recognized its main drawback: as states and transitions are added, the code can become tangled and difficult to maintain.
Today, we will directly address that problem. Our learning outcome is to refactor a conditional-based FSM into a table-driven FSM using Luau tables. We will replace our large if-elseif block with a more organized, scalable, and powerful structure. This is a fundamental step toward building the robust, reusable FSMs needed for your goals in NPC AI, UI, and game state management.
1. The Problem with Large Conditionals
As a quick refresher on the problem we're solving, let's look at a classic article on game design patterns. It walks through a scenario of adding behaviors to a game character. As new actions like jumping, ducking, and diving are added, the code becomes a maze of boolean flags and nested if statements.
State · Design Patterns Revisited - Game Programming Patterns
Please read a short section from the book Game Programming Patterns by Robert Nystrom. It perfectly illustrates how quickly conditional logic can become unmanageable when managing character states.
Read the section titled 'We’ve All Been There'. Pay close attention to how each new feature requires adding more flags and conditions, leading to bugs.
This spiral of complexity is what we want to break. A table-driven approach allows us to add and change states without rewriting the core logic of our machine.
2. Introducing the Table-Driven FSM
The core idea of a table-driven FSM is simple but powerful: instead of writing state logic in conditional blocks, we will define our states and their transitions within a Luau table.
Here's how it works:
- We create a main
statestable. - Each key in this table is a state's name (e.g.,
"Locked"). - The value associated with each key is another table containing all the information about that state, most importantly, its transitions.
Let's look at the if-elseif code from a Roblox DevForum post that mirrors the kind of FSM we built in the last lesson.
Turn Based Combat System - Scripting Support - Developer Forum
This post on the Roblox Developer Forum discusses a turn-based combat system. One user provides an initial script that uses if-elseif logic, which is a perfect example of what we want to refactor.
Quickly review the code block in this section. Notice the while loop with if-elseif statements checking the currentState. This is the pattern we are about to replace.
With a table-driven approach, that entire while loop's logic can be replaced. Instead of checking if currentState == "BattleStart", the machine will simply look up "BattleStart" in its states table and find the logic it needs there.
The main benefits are:
- Decoupling: The FSM's transition function is no longer hardcoded with knowledge of every state.
- Scalability: Adding a new state means adding a new entry to the
statestable, not modifying the core FSM logic. - Organization: All logic for a single state (its transitions, and as you'll see later, its behaviors) is grouped together.
3. Hands-On: Refactoring the Turnstile FSM
Let's refactor the turnstile FSM from our previous lesson.
Here is the complete, refactored code. Create a new Script in ServerScriptService, paste this in, and we will walk through the changes.
-- 1. Define the states and their transitions in a table
local states = {
Locked = {
-- This state has one possible transition:
-- On the "Coin" event, it moves to the "Unlocked" state.
transitions = {
Coin = "Unlocked"
}
},
Unlocked = {
-- This state has one possible transition:
-- On the "Push" event, it moves to the "Locked" state.
transitions = {
Push = "Locked"
}
}
}
-- 2. Set the initial state of the machine
local currentStateName = "Locked"
print("Initial state: " .. currentStateName)
-- 3. Create a function to process events using the states table
function handleEvent(event)
print("-------------------------")
print("Event received: " .. event)
print("State before: " .. currentStateName)
-- Get the data for the current state from our main 'states' table
local currentStateData = states[currentStateName]
if not currentStateData then
warn("Warning: Current state '"..tostring(currentStateName).."' does not exist.")
return
end
-- Look for a valid transition for the given event in the current state's data
local nextStateName = currentStateData.transitions[event]
-- If a valid next state was found, update our current state
if nextStateName then
print("Transitioning from '" .. currentStateName .. "' to '" .. nextStateName .. "'")
currentStateName = nextStateName
else
print("No transition found for event '" .. event .. "' in state '" .. currentStateName .. "'. No change.")
end
print("State after: " .. currentStateName)
end
-- 4. Simulate a sequence of events to test the FSM
wait(1)
handleEvent("Push") -- Should do nothing
wait(1)
handleEvent("Coin") -- Should unlock
wait(1)
handleEvent("Coin") -- Should do nothing
wait(1)
handleEvent("Push") -- Should lock
wait(1)
handleEvent("Push") -- Should do nothing
print("-------------------------")
print("Simulation finished.")
Analysis of the Refactored Code:
- The
statesTable: At the top, we define our entire FSM's structure.states.Locked.transitions.Coinpoints to"Unlocked". This is far more declarative and easier to read than anifstatement. - The
handleEventFunction: Compare this to the previous lesson's version. It's now generic! It doesn't contain any logic specific to "Locked" or "Unlocked". It simply performs a lookup:states[currentStateName]gets the data for the current state.currentStateData.transitions[event]finds the next state based on the event.- If
nextStateNameis found, it updates the state.
If we wanted to add a "Broken" state, we wouldn't touch handleEvent at all. We would simply add a new entry to the states table. This is the power of a table-driven design.
4. A More Formal Table-Driven Structure
Our refactored script is a great step forward. In professional game development, this pattern is often formalized into a reusable ModuleScript.
Let's look at another example from the same DevForum thread. This user provides a more advanced, OOP-style StateMachine module.
Turn Based Combat System - Scripting Support - Developer Forum
Now, let's look at a more structured example from the same DevForum thread. This code wraps the table-driven logic into a reusable ModuleScript.
Read the code for the StateMachine module. Focus on the States table inside the constructor and the Add function. See how it's designed to hold a collection of states, just like our simpler example. Don't worry about the Push, Pop, or Peek functions for now; that's a more advanced pattern called a stack-based FSM we may touch on later.
This module encapsulates the core idea: it has a States table that it populates via an Add method. This is the foundation for the reusable FSM module we will be building in Module 3.
To see this table-driven structure explained in a video format, check out the following resource.
State Machines Can Make Your Roblox Game Better!
This video by qweekertom provides a very clear, step-by-step implementation of a class-based FSM in Roblox, which is an excellent example of a table-driven approach.
Please watch from 2:00 to 2:47 and then from 6:49 to 8:01. In the first segment, pay close attention to the goto function and how it retrieves oldState and nextState from a table. The second segment is a review that clearly summarizes how the states table holds each state's properties and how the goto and update functions use it.
The video reinforces the key pattern: a central states table stores the definitions, and the FSM's core functions (goto, update) operate by looking up information in that table rather than using hardcoded conditional logic.
Conclusion
In this lesson, you took a crucial step from a basic FSM implementation to a professional, scalable design pattern. By refactoring our if-elseif logic into a table-driven structure, you've made your FSM easier to read, manage, and expand.
Key Takeaways:
- A table-driven FSM uses a Luau table to define states and their transitions.
- This approach decouples the FSM's core logic from the state-specific logic.
- To add or modify states, you change the data in the
statestable, not thehandleEventfunction's code. - This pattern is highly scalable and forms the basis for creating reusable FSM modules.
Our state definitions are still just data. In the next lesson, we will make them much more dynamic. We will learn how to implement enter, update, and exit callbacks for each state, allowing us to attach executable code that runs automatically during state changes.