Skip to main content

Welcome to a Grasp lesson

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

Lesson 6

FSM State Callbacks

Recommended resources

Hello! Welcome back to our course on Finite State Machines.

In our last lesson, we made a significant leap by refactoring our simple turnstile FSM from a series of if-elseif statements into a much cleaner, more scalable, table-driven structure. You learned how to define states and their allowed transitions as data within a Luau table, which is a core concept in building robust FSMs.

However, our states are still just names in a table. They don't do anything on their own. Today, we'll change that. The learning outcome for this lesson is to implement enter, update, and exit callbacks for each state in a table-driven FSM. We are going to give our states behavior by attaching functions that run automatically at key moments in the state's lifecycle.

1. The State Lifecycle: Enter, Update, Exit

So far, our FSM knows how to move from one state to another. Now, we need to define what happens when a state becomes active, while it is active, and when it ceases to be active. We accomplish this with three fundamental callback functions:

  • OnEnter: A function that runs once when the FSM transitions into a state. This is perfect for setup tasks, like starting an animation, playing a sound, or making a UI element visible.
  • OnExit: A function that runs once when the FSM transitions out of a state. This is the counterpart to OnEnter and is used for cleanup: stopping an animation, reversing a UI change, etc.
  • OnUpdate: A function that runs repeatedly, typically on every game tick or frame, as long as the FSM remains in that state. This is where you put the state's ongoing logic, such as moving an NPC, checking for player proximity, or monitoring a timer.

Given your background in Object-Oriented Programming, you can think of these as being conceptually similar to a class's methods: OnEnter is like a constructor, OnExit is like a "destructor" or cleanup method, and OnUpdate is like a method that gets called in a main game loop.

This diagram provides a simple visual for the entry and exit actions.

Entry and Exit Actions in State Machine Diagrams
This diagram shows that when a book's state changes to "on loan", an `entry` action `myBkCopy.borrow()` is called. Conversely, when it exits that state, an `exit` action `myBkCopy.returned()` might be called. This is the core principle of `OnEnter` and `OnExit` callbacks.

2. Defining Callbacks in Luau

To implement this, we'll expand our state definitions in the main states table. Instead of a state's value just being a table of transitions, it will now be a table holding the transitions and our new callback functions.

Many developers follow this pattern. The following resource from a Stack Overflow discussion on Roblox FSMs shows a clear, practical implementation.

Is this a correct way to implement a basic FSM in Lua for my FSM diagram

This post shows a great way to structure a state module in Luau. Let's focus on how it defines a state and handles the transition logic.

First, look at the 'State Module' code. Notice how the State:New() function creates a table with default Init, Update, Enter, and Exit functions. Next, look at the 'StateMachine Module' and its SetState function. See how it explicitly calls self.currentState:Exit() before changing the state, and then self.currentState.Enter() on the new state. This is the classic execution order.

This is the exact pattern we will use. We'll add OnEnter, OnExit, and OnUpdate functions to each state's definition table.


3. Hands-On: Upgrading the Turnstile FSM

Let's apply this to the turnstile FSM we refactored in the last lesson. We will add callbacks that print messages to the console. This will let us clearly see when each callback is fired.

Create a new Script in ServerScriptService and paste in the following code.

local RunService = game:GetService("RunService")

-- 1. Define states with transitions AND callbacks
local states = {
    Locked = {
        transitions = {
            Coin = "Unlocked"
        },
        OnEnter = function()
            print("▶️ ENTER: Locked. The turnstile is now locked.")
        end,
        OnExit = function()
            print("◀️ EXIT: Locked. Leaving the locked state.")
        end,
        OnUpdate = function(deltaTime)
            -- This function runs every frame while in the 'Locked' state.
            -- We could, for example, play a continuous red light animation here.
        end
    },
    Unlocked = {
        transitions = {
            Push = "Locked"
        },
        OnEnter = function()
            print("▶️ ENTER: Unlocked. You may now push the turnstile.")
        end,
        OnExit = function()
            print("◀️ EXIT: Unlocked. Leaving the unlocked state.")
        end,
        OnUpdate = function(deltaTime)
            -- This function runs every frame while in the 'Unlocked' state.
            -- We could, for example, have a green light flashing here.
        end
    }
}

-- 2. Set the initial state
local currentStateName = "Locked"

-- Manually call OnEnter for the very first state
print("FSM Initializing... ")
states[currentStateName].OnEnter()


-- 3. Modify the handleEvent function to use callbacks
function handleEvent(event)
    print("-------------------------")
    print("EVENT received: " .. event)

    -- Get the data for the current state
    local currentStateData = states[currentStateName]
    
    -- Find the next state based on the event
    local nextStateName = currentStateData.transitions[event]

    -- If a valid transition exists, execute the change
    if nextStateName then
        -- Call OnExit for the state we are LEAVING
        if currentStateData.OnExit then
            currentStateData.OnExit()
        end

        -- Update the state name
        currentStateName = nextStateName
        local nextStateData = states[currentStateName]
        
        -- Call OnEnter for the state we are ENTERING
        if nextStateData.OnEnter then
            nextStateData.OnEnter()
        end
    else
        print("No transition found for event '" .. event .. "' in state '" .. currentStateName .. "'.")
    end
end

-- 4. Create an update loop to call OnUpdate
RunService.Heartbeat:Connect(function(deltaTime)
    local currentStateData = states[currentStateName]
    if currentStateData and currentStateData.OnUpdate then
        currentStateData.OnUpdate(deltaTime)
    end
end)


-- 5. Simulate events to test the callbacks
wait(2)
handleEvent("Push")  -- Should do nothing
wait(2)
handleEvent("Coin")  -- Should transition: Locked.OnExit -> Unlocked.OnEnter
wait(2)
handleEvent("Coin")  -- Should do nothing
wait(2)
handleEvent("Push")  -- Should transition: Unlocked.OnExit -> Locked.OnEnter

Analysis of the Changes:

  1. State Definitions: The states table now includes OnEnter, OnExit, and OnUpdate functions for both Locked and Unlocked.
  2. handleEvent Function: The core of the change is here. When a valid transition is found:
    • First, it calls OnExit on the current state.
    • Then, it updates the currentStateName.
    • Finally, it calls OnEnter on the new state. This exit-before-enter order is crucial for preventing conflicts and ensuring clean state transitions.
  3. Update Loop (RunService.Heartbeat): This is a new and vital piece. We connect a function to RunService.Heartbeat, which Roblox fires every frame. Inside this connection, we find the current state's data and call its OnUpdate function. This is how you implement logic that needs to run continuously. We pass deltaTime (the time since the last frame) which is essential for frame-rate independent physics or animations.
  4. Initial OnEnter call: Notice that we have to manually call OnEnter for the starting state, because no handleEvent transition leads into it at the beginning.

Run this script and examine the output. You will see the OnEnter and OnExit messages appear exactly when a state change occurs, proving our callback system works.


4. Callbacks in Action

To see a more complex and practical application of callbacks, let's watch a short segment of a video. The creator uses a similar system with slightly different naming (beforeLeave, afterEnter) to control a UI, which is one of your specific goals.

Roblox Studio - Finite State Machine EXPLAINED!

This video by CoodSlayer demonstrates how callbacks can be used to manage UI animations and visibility during state changes. It's a great preview of what we'll be building in Module 5.

First, watch from 2:36 to 4:21. The creator establishes the order of execution for their callbacks inside the changeState function. Then, watch from 7:05 to 10:19. Here, callbacks are defined to handle applying and removing highlights from UI buttons. This is a perfect example of using OnEnter to add a visual effect and OnExit to remove it.

This example should give you a clear idea of how you can use these callbacks to implement features from your own goals, like making an NPC start its Patrol animation in OnEnter and stop it in OnExit, or using TweenService to fade a UI panel in and out.


Conclusion

Today you've fundamentally enhanced our FSM. By adding OnEnter, OnUpdate, and OnExit callbacks, you've transformed states from passive data containers into active, behavioral components that can execute code.

Key Takeaways:

  • States have a lifecycle managed by three core callbacks:
    • OnEnter: For one-time setup when a state begins.
    • OnExit: For one-time cleanup when a state ends.
    • OnUpdate: For continuous logic that runs every frame while the state is active.
  • The correct transition sequence is to call OnExit on the old state before calling OnEnter on the new state.
  • Ongoing state logic is handled by calling the OnUpdate function from a central game loop, such as RunService.Heartbeat.

Our handleEvent function is doing a lot of work right now: checking for transitions, calling OnExit, changing the state, and calling OnEnter. In the next lesson, we will clean this up by creating a dedicated transition function that encapsulates all this logic, making our FSM even more modular and easier to read.