-
Notifications
You must be signed in to change notification settings - Fork 0
Examples
A source-driven cookbook of real patterns from the repository and current runtime APIs.
Important
This page intentionally favors real repository examples and current runtime behavior over older pseudo-code. Where older example files need modernization, this page calls that out explicitly.
Source: example/TrafficLightFSMExample.luau
local TrafficLightFSM = {
Name = "TrafficLightFSM",
ValidStates = { "Red", "Green", "Yellow", "Blink", "Maintenance" },
TerminalStates = {},
InitialState = "Red",
}
function TrafficLightFSM:RegisterStates()
self:AddState("Red", function(fsm)
fsm.Context.Cycles = (fsm.Context.Cycles or 0) + 1
fsm:ScheduleTransition(4, "Green")
end, { "Green", "Maintenance" })
self:AddState("Green", function(fsm)
fsm:ScheduleTransition(5, "Yellow")
end, { "Yellow", "Maintenance" })
self:AddState("Yellow", function(fsm)
local cycles = fsm.Context.Cycles or 0
if cycles % 3 == 0 and not fsm.Context._blinkedThisCycle then
fsm.Context._blinkedThisCycle = true
fsm:ScheduleTransition(2, "Blink")
else
fsm.Context._blinkedThisCycle = false
fsm:ScheduleTransition(3, "Red")
end
end, { "Red", "Blink", "Maintenance" })
self:AddState("Blink", function(fsm)
fsm:ScheduleTransition(2, "Yellow")
end, { "Yellow", "Blink", "Maintenance" })
self:AddState("Maintenance", function(fsm)
-- manual stop state
end, { "Red" })
end
return TrafficLightFSMIt demonstrates the cleanest possible use of the current timing API:
- no manual
task.delay - no stale closure guards
- a simple state graph that ServiceManager visualizes nicely
- context state (
Cycles,_blinkedThisCycle) used exactly where it belongs
[*] ──→ Red
Red ──after 4s──→ Green
Green ──after 5s──→ Yellow
Yellow ──every 3rd cycle──→ Blink
Yellow ──otherwise after 3s──→ Red
Blink ──after 2s──→ Yellow
Red ─────────→ Maintenance
Green ───────→ Maintenance
Yellow ──────→ Maintenance
Blink ───────→ Maintenance
Maintenance ─→ Red
The repository's test/main.server.luau already creates a demo instance of this machine:
local trafficFsm = Orchestrator.CreateStateMachine({
StateMachineClass = "TrafficLightFSM",
StateMachineId = "TrafficLight_Demo",
Context = { Cycles = 0 },
})Then the client bootstrap launches ServiceManager, making the graph visible immediately in Studio.
Sources:
example/DoorEntityExample.luauexample/DoorStateMachineExample.luauexample/GameControllerExample.luau
This is the repository's most complete “real workflow” example because it combines:
- an entity with domain methods (
Open,Close,Reset) - replication-aware visual updates
- an FSM with validation, failure, and cleanup
- a controller script that creates jobs and reacts to completion
Core idea from DoorEntityExample.luau:
local DoorEntity = {
Name = "DoorEntity",
Replication = {
Enabled = true,
RateLimit = 60,
},
Schema = {
IsOpen = { Type = "boolean", Replicate = true },
Locked = { Type = "boolean", Replicate = true },
Color = { Type = "Color3", Replicate = true },
_hinge = { Type = "BasePart" },
_door = { Type = "BasePart" },
_statusLight = { Type = "BasePart" },
},
}
function DoorEntity:GetContext(params)
local hinge = self.Instance:WaitForChild("Hinge", 5)
local statusLight = self.Instance:WaitForChild("StatusLight", 5)
return self({
_hinge = hinge,
_statusLight = statusLight,
IsOpen = self.IsOpen or false,
Locked = self.Locked or true,
})
end
function DoorEntity:Open()
self.IsOpen = true
self.Color = Color3.fromRGB(0, 255, 0)
self.Locked = false
return self:UpdateEntity()
endThe example file is conceptually useful, but for current runtime correctness you should add:
Mutable = true,because UpdateEntity() requires a mutable entity in the current BaseEntity implementation.
The DoorStateMachineExample.luau example models a job-like workflow:
local DoorStateMachine = {
Name = "DoorStateMachine",
ValidStates = { "Validate", "CreatingMessage", "Opening", "Closing", "Completed", "Failed" },
TerminalStates = { "Completed", "Failed" },
InitialState = "Validate",
}It then does the following:
- validates the command request
- creates UI feedback for the user
- opens or closes the door through entity methods
- polls the result with
WaitSpan = 0.5 - finishes or fails cleanly
[*] ──→ Validate
Validate ──valid request──→ CreatingMessage
Validate ──invalid request──→ Failed
CreatingMessage ──DesiredState = Open──→ Opening
CreatingMessage ──DesiredState = Close──→ Closing
CreatingMessage ──UI setup failure──→ Failed
Opening ──WaitSpan polling──→ Opening
Closing ──WaitSpan polling──→ Closing
Opening ──→ Completed
Closing ──→ Completed
Opening ──→ Failed
Closing ──→ Failed
Completed ──→ [*]
Failed ─────→ [*]
example/GameControllerExample.luau shows how a gameplay script stitches the pieces together:
local Door1 = Orchestrator.CreateEntity({
EntityClass = "DoorEntity",
EntityId = "Door1",
Context = {
Name = doorModel.Name,
Instance = doorModel,
OwnerId = script.Name,
},
})
local openJob = Orchestrator.CreateStateMachine({
StateMachineClass = "DoorStateMachine",
StateMachineId = script.Name .. " DoorStateMachine " .. tick(),
Context = {
DoorEntity = Door1,
DesiredState = "Open",
},
})Then it connects Completed and Failed signals to chain the close job.
The example file manually calls :Start({ State = "Validate" }) after CreateStateMachine(). With the current factory, that is now optional unless you explicitly pass AutoStart = false.
Source features:
Core/Factory/BehaviorTree.luauServiceManager/ServiceBrain.luau
The built-in BehaviorTree module currently exposes:
SelectorSequenceInverterSucceederConditionSetState
local BehaviorTree = require(game:GetService("ReplicatedStorage").Orchestrator.Core.Factory.BehaviorTree)
local DecideState = BehaviorTree.Selector({
BehaviorTree.Sequence({
BehaviorTree.Condition(function(ctx)
return ctx.Entity.Health <= 0
end),
BehaviorTree.SetState("Dead"),
}),
BehaviorTree.Sequence({
BehaviorTree.Condition(function(ctx)
return ctx.TargetVisible == true
end),
BehaviorTree.SetState("Chase"),
}),
BehaviorTree.Sequence({
BehaviorTree.Condition(function(ctx)
return ctx.HasPatrolRoute == true
end),
BehaviorTree.SetState("Patrol"),
}),
BehaviorTree.SetState("Idle"),
})You can then call that tree from a lightweight decision state:
self:AddState("Think", function(fsm)
DecideState(fsm)
end, { "Dead", "Chase", "Patrol", "Idle" })A BehaviorTree is a good front-end for choosing which FSM state to enter when the decision logic is more complex than a couple of if statements.
Use it when:
- many conditions compete for control
- you want reusable decision leaves
- you want declarative “decision first, state transition second” logic
ServiceManager/ServiceBrain.luau uses a BT sequence/selector composition to decide whether to fetch data, run insight analysis, re-render UI, and tick animations.
That is an important architectural clue: the BehaviorTree system is not theoretical; the framework uses it in production code.
Source feature: BaseStateMachine:AddSubMachine()
A good HFSM pattern is to let the parent own phase-level behavior while the child owns the details.
Note
The following is a conceptual pattern example — it is not shipped in the example/ folder. For shipped examples, see DoorEntityExample.luau, DoorStateMachineExample.luau, and TrafficLightFSMExample.luau.
local BossFSM = {
Name = "BossFSM",
ValidStates = { "Intro", "Combat", "Recover", "Dead" },
InitialState = "Intro",
TerminalStates = { "Dead" },
}
function BossFSM:RegisterStates()
self:AddState("Intro", function(fsm)
fsm:ScheduleTransition(2, "Combat")
end, { "Combat" })
self:AddSubMachine("Combat", CombatPhaseFSM, {
InitialState = "Approach",
Transitions = {
OnCompleted = "Recover",
OnFailed = "Dead",
OnCancelled = "Recover",
},
StoreReference = "CombatMachine",
})
self:AddState("Recover", function(fsm)
fsm:ScheduleTransition(3, "Combat")
end, { "Combat", "Dead" })
self:AddState("Dead", function(fsm)
fsm:Finish()
end)
endlocal CombatPhaseFSM = {
Name = "CombatPhaseFSM",
ValidStates = { "Approach", "Attack", "Done", "Fail" },
InitialState = "Approach",
TerminalStates = { "Done", "Fail" },
}
function CombatPhaseFSM:RegisterStates()
self:AddState("Approach", {
OnHeartbeat = function(state, fsm, dt)
if fsm.Context.DistanceToTarget < 8 then
fsm.State = "Attack"
end
end,
}, { "Attack", "Fail" })
self:AddState("Attack", {
OnHeartbeat = function(state, fsm, dt)
if fsm.Context.TargetDefeated then
fsm:Finish()
elseif fsm.Context.Entity.Health <= 0 then
fsm:Fail("boss died during combat")
end
end,
})
end- the parent stays readable
- the child remains reusable
- context is shared automatically
- completion/failure/cancel paths are explicit
BossFSM CombatPhaseFSM
│ │
│── create sub-machine on enter Combat ─→│
│ │── Start({State="Approach"})
│←── Completed ───────────────│
│── ChangeState({Name="Recover"})
│── OnLeave -> Cancel if still active ─→│
Source features:
Orchestrator:StartServiceManager()ServiceManager.SwitchSubsystem(name)ServiceManager.SetContextMode(mode)-
ConsoleModulebuilt-in commands
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Orchestrator = require(ReplicatedStorage:WaitForChild("Orchestrator"))
Orchestrator:RegisterComponents()
if RunService:IsStudio() then
local sm = Orchestrator:StartServiceManager()
sm.SwitchSubsystem("FSM")
sm.SetContextMode("server")
end- boot the place
- open
FSMin server mode to inspect authoritative state - open
ENTITYto inspect schema values and versions - open
NETWORKto inspectEntityUpdateEventor request callbacks - open
PROFILERto see if the heartbeat or a task is hot - use
CONSOLEcommands such as:tasks listfsm info TrafficLight_Demoentities info Door1perfswitch network
If you want a practical demo setup with almost no extra code, use the repository's shipped bootstraps:
test/main.server.luautest/Client.client.luau
The server script does:
local Orchestrator = require(ReplicatedStorage:WaitForChild("Orchestrator"))
Orchestrator:RegisterComponents()
local trafficFsm = Orchestrator.CreateStateMachine({
StateMachineClass = "TrafficLightFSM",
StateMachineId = "TrafficLight_Demo",
Context = { Cycles = 0 },
})The client script does:
local Orchestrator = require(ReplicatedStorage:WaitForChild("Orchestrator", 30))
Orchestrator:RegisterComponents()
Orchestrator:StartServiceManager()That combination is the easiest way to verify the framework end to end in Studio.
| Need | Best example |
|---|---|
| simple timed transitions | TrafficLightFSMExample |
| entity methods + visuals + workflow FSM |
DoorEntityExample + DoorStateMachineExample
|
| orchestration script that chains jobs | GameControllerExample |
| decision trees over many conditions | BehaviorTree composition |
| parent/child phase decomposition | HFSM AddSubMachine pattern |
| live runtime introspection | ServiceManager bootstrap + console |
When adapting the shipped examples into real gameplay code, make these updates immediately:
- add
Mutable = trueto any entity that commits withUpdateEntity() - remove redundant manual
:Start()calls unlessAutoStart = false - replace ad-hoc polling with
ScheduleTransition()when the logic is a fixed timed exit - gate
StartServiceManager()behind Studio/dev checks - assign stable IDs intentionally rather than using
tick()everywhere
Quick Links: Home · Quick Start · API Reference · Architecture · Examples · Glossary
Copyright: © 2026 RBXStateMachine contributors · Repository · License information