Hello! Welcome back to our course on building a reusable Finite State Machine in Roblox.
In the last lesson, we established the "map" of our FSM by creating a defineTransitions method. This gave our machine a static set of rules about which states can be reached from others. However, our FSM is still just a blueprint; it can't actually move from one state to another.
Today, we'll build the engine that drives the FSM. Our goal is to implement a method to trigger a state transition, including support for guard conditions. This function will be the heart of our module, responsible for validating a requested change and executing it by firing the appropriate exit and enter callbacks.
1. The Core Transition Method
First, let's create the central method that will handle all state changes. We'll call it transitionTo. Its basic job is to move the FSM from its current state to a new one, but only if the rules we defined last time allow it.
The logic for a transition from state A to state B follows a precise sequence:
- Verify that a transition from A to B is permitted.
- If it is, call the
exitfunction for state A. - Update the FSM's current state to be B.
- Finally, call the
enterfunction for state B.
Let's add a basic transitionTo method to our FSM ModuleScript. This version will only check the rules from our transitions table.
-- In your FSM ModuleScript
function FSM:transitionTo(newStateName)
-- First, check if the transition is even possible according to our rules
local validFromStates = self.transitions[newStateName]
local isPermitted = validFromStates and validFromStates[self.currentStateName]
if not isPermitted then
warn("FSM Error: Transition from '" .. self.currentStateName .. "' to '" .. newStateName .. "' is not permitted.")
return -- Stop here if the transition is illegal
end
-- If we're already in the target state, do nothing.
if self.currentStateName == newStateName then
return
end
-- Get the state definitions
local oldState = self.states[self.currentStateName]
local newState = self.states[newStateName]
-- Call the exit callback on the old state, if it exists
if oldState and oldState.exit then
oldState.exit()
end
local oldStateName = self.currentStateName
self.currentStateName = newStateName
-- Call the enter callback on the new state, if it exists
if newState and newState.enter then
-- Pass the previous state's name to the enter function
newState.enter(oldStateName)
end
end
This is a great start. We now have a function that safely changes the state and orchestrates the exit and enter callbacks.
2. Adding Power with Guard Conditions
Our current system can answer the question, "Can I go from Chase to Attack?" But in a real game, you need to answer a more specific question: "Should I go from Chase to Attack right now?"
For example, an NPC should only transition to "Attack" if the player is within range. This is a guard condition: an additional, dynamic check that must pass for a transition to occur.
Take a look at this combat FSM diagram. The labels on the arrows are guard conditions.

A transition from Guarding to Stunned is only triggered if the guard was parried. A transition from Attacking to Idle might only happen if the attack animation has finished. Guards give us this fine-grained, conditional control.
3. Implementing Guards in Luau
To implement guards, we need to do two things:
- Update
defineTransitionsto accept an optionalguardfunction for each rule. - Update
transitionToto execute thisguardfunction before proceeding.
Modifying defineTransitions
Let's modify our transition definitions to include a guard field. Our transitions table inside the FSM will now store the guard function itself (or true if no guard is provided).
-- In your FSM ModuleScript, replace your existing defineTransitions function
function FSM:defineTransitions(definitions)
for _, definition in ipairs(definitions) do
local from = definition.from
local to = definition.to
local guard = definition.guard -- The new guard function
if not self.transitions[to] then
self.transitions[to] = {}
end
local rule = guard or true -- Store the function, or 'true' as a placeholder
if type(from) == "table" then
for _, fromState in ipairs(from) do
self.transitions[to][fromState] = rule
end
elseif type(from) == "string" then
self.transitions[to][from] = rule
end
end
end
Modifying transitionTo
Now, let's create the final version of transitionTo. This function will now check for and execute the guard. We will also add support for passing arguments through the transition, which can be useful for both the guard and the new state's enter callback.
-- In your FSM ModuleScript, replace your existing transitionTo function
function FSM:transitionTo(newStateName, ...)
if self.currentStateName == newStateName then
return
end
local fromStates = self.transitions[newStateName]
local rule = fromStates and fromStates[self.currentStateName]
if not rule then
warn("FSM Error: Transition from '" .. self.currentStateName .. "' to '" .. newStateName .. "' is not permitted.")
return
end
-- Check the guard condition if it's a function
if type(rule) == "function" then
if not rule(...) then -- If the guard returns false, abort the transition
return
end
end
local oldState = self.states[self.currentStateName]
local newState = self.states[self.newStateName]
if oldState and oldState.exit then
oldState.exit(newStateName) -- Pass the upcoming state to the exit function
end
local oldStateName = self.currentStateName
self.currentStateName = newStateName
if newState and newState.enter then
newState.enter(oldStateName, ...) -- Pass the previous state and any other args
end
end
Notice how rule(...) is called. If the rule is a function (our guard), it gets executed. If it returns false (or nil), the transition is silently stopped. This is a powerful and clean way to control your FSM's logic.
The following video provides an excellent real-world example. The developer refactors an NPC's AI to use an FSM and implicitly uses guard conditions to decide when to transition, such as when a player's "friend score" changes.
NPC AI - How to Create a Finite State Machine for AI
In this video from SimTek Game Development, pay close attention to how external events and changing conditions are used to trigger state changes. This is the essence of what guard conditions do.
Watch the segment from 16:47 to 19:50. The receiveChat function acts as an external trigger. Notice the if not wasFriend and isFriend check. This is a perfect example of a guard condition: the transition to the 'Greet' state only happens if the NPC's friendship status just changed.
4. Putting It All Together
Let's test our new system with a practical example. Imagine an enemy AI with three states: Patrolling, Chasing, and Attacking.
- It can go from
PatrollingtoChasingonly if a target is visible. - It can go from
ChasingtoAttackingonly if the target is within a certain range.
Let's update our test Script to model this.
-- In a regular 'Script' in ServerScriptService
local FSM = require(script.Parent.FSM)
-- A dummy 'enemy' object with some properties for our guards to check
local enemy = {
isTargetVisible = false,
targetDistance = 100
}
-- Create the FSM
local enemyAI = FSM.new("Patrolling")
-- Define states
enemyAI:defineState("Patrolling", {
enter = function(previousState) print("Starting to Patrol. Came from: " .. tostring(previousState)) end
})
enemyAI:defineState("Chasing", {
enter = function(previousState) print("Target spotted! Chasing now. Came from: " .. previousState) end
})
enemyAI:defineState("Attacking", {
enter = function(previousState) print("In range! Attacking! Came from: " .. previousState) end
})
-- Define transitions with guards
enemyAI:defineTransitions({
{ from = "Patrolling", to = "Chasing", guard = function()
return enemy.isTargetVisible
end },
{ from = "Chasing", to = "Patrolling", guard = function()
return not enemy.isTargetVisible
end },
{ from = "Chasing", to = "Attacking", guard = function(distance)
-- This guard uses an argument passed during the transition
return distance <= 10
end },
{ from = "Attacking", to = "Chasing", guard = function(distance)
return distance > 10
end }
})
-- Let's simulate some scenarios
print("--- SCENARIO 1: Target not visible ---")
print("Current State:", enemyAI.currentStateName)
print("Attempting to transition to Chasing...")
enemyAI:transitionTo("Chasing") -- This will fail because the guard is false
print("State after attempt:", enemyAI.currentStateName, "\n")
wait(2)
print("--- SCENARIO 2: Target becomes visible ---")
enemy.isTargetVisible = true
print("Current State:", enemyAI.currentStateName)
print("Attempting to transition to Chasing...")
enemyAI:transitionTo("Chasing") -- This will succeed
print("State after attempt:", enemyAI.currentStateName, "\n")
wait(2)
print("--- SCENARIO 3: Target is too far to attack ---")
enemy.targetDistance = 20
print("Current State:", enemyAI.currentStateName)
print("Attempting to transition to Attacking, passing distance:", enemy.targetDistance)
enemyAI:transitionTo("Attacking", enemy.targetDistance) -- This will fail
print("State after attempt:", enemyAI.currentStateName, "\n")
wait(2)
print("--- SCENARIO 4: Target moves into range ---")
enemy.targetDistance = 8
print("Current State:", enemyAI.currentStateName)
print("Attempting to transition to Attacking, passing distance:", enemy.targetDistance)
enemyAI:transitionTo("Attacking", enemy.targetDistance) -- This will succeed
print("State after attempt:", enemyAI.currentStateName, "\n")
When you run this script, you'll see how the FSM correctly blocks or allows transitions based on the return values of our guard functions. This pattern is extremely common, and many professional FSM libraries for Roblox, like the popular "Pulse" module, feature a similar concept, often called CanTransition.
Conclusion
Fantastic work! You have now implemented the most critical piece of our reusable FSM module. The transitionTo method is the engine that brings your state machine to life, and the addition of guard conditions gives it the intelligence to make dynamic, context-aware decisions.
Key Takeaways:
- A
transitionTomethod is the public-facing function used to request a state change. - The transition process follows a strict order: check rules, call
exit, change state, callenter. - Guard conditions are functions that return
trueorfalse, allowing you to add dynamic logic to your transition rules. - Guards check conditions that are only known at runtime (e.g., player distance, health, animation status).
- Passing arguments through
transitionTois a powerful way to provide context to both guard functions andentercallbacks.
We now have all the core pieces of our FSM module: a constructor, a way to define states, a way to define transition rules, and a way to execute those transitions. In our next lesson, we will put it all to the test by writing a dedicated test script to verify all its functionality and ensure our module is robust and ready for use in a real game.