Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Table-Driven FSMs in Luau

Recommended resources

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:

  1. We create a main states table.
  2. Each key in this table is a state's name (e.g., "Locked").
  3. 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 states table, 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:

  1. The states Table: At the top, we define our entire FSM's structure. states.Locked.transitions.Coin points to "Unlocked". This is far more declarative and easier to read than an if statement.
  2. The handleEvent Function: 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 nextStateName is 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 states table, not the handleEvent function'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.