An intuitive fully-typed Finite State Machine in Luau that supports async transitions by queueing events, developed for use in Roblox experiences.
This project is licensed under the terms of the MIT license. See LICENSE.md for details.
Tests need to be written and the API may receive small changes while this project is being finalized for a first release.
A Finite State Machine (FSM) provides a way to enforce specific logical flow among a set of States. Given an Event, the FSM responds by looking up the corresponding Transition for that Event in its current State. A Transition is a callback function that is invoked when an Event is given to the FSM, and it returns the next State for the FSM to move to.
The FSM enforces that Events can only be called when a Transition is defined for that Event in its current State by erroring if an Event is called in an invalid state (a state with no Transition defined for that Event).
The FSM provides 5 signals that can be listened to during handling of an event. These signals, transition callbacks, and state changes are processed in the following order:
- Fire
beforeEvent
signal- with arguments
eventName
,beforeState
- with arguments
- Call
transition.beforeAsync()
(required, returns next state)- with the VarArgs from
:handle(eventName, transitionArgs...)
- with the VarArgs from
- Fire
leavingState
signal- with arguments
beforeState, afterState
- with arguments
- Update
_currentState
to next state - Fire
stateEntered
signal- with arguments
afterState, beforeState
- with arguments
- Call
transition.afterAsync()
(if specified)- with the VarArgs from
:handle(eventName, transitionArgs...)
- with the VarArgs from
- Fire
afterEvent
signal- with arguments
eventName, afterState, beforeState
- with arguments
- Fire
finished
signal if next state frombeforeAsync()
wasnil
- with argument
beforeState
- with argument
Transitions can be asynchronous, which is supported by queuing each Event submitted via :handle() and processing them in First-In-First-Out (FIFO) order. The next Event starts processing immediately after the previous Event's handler fires afterEvent
.
The FSM can Finish if a Transition does not return a "next state" during an event marked as "canBeFinal".
In such a case, the FSM will fire a finished
event and will error if any further Events are handled.
A nil
state means the FSM has Finished.
A simple state machine diagram for a light switch may look like this, where
- States are represented as rectangles, and squares indicate the State can be Final
- Events are represented as capsules
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StateQ = require(ReplicatedStorage.Packages.StateQ)
local LightState = {
On = "On",
Off = "Off",
}
local Event = {
SwitchOn = "SwitchOn",
SwitchOff = "SwitchOff",
}
local light = StateQ.new(LightState.On, {
[Event.SwitchOn] = {
canBeFinal = true,
from = {
[LightState.Off] = { -- From state
beforeAsync = function() -- Transition
print("Light is transitioning to On")
for i = 1, 100 do
-- do some action to increase brightness of a light here
task.wait()
end
return LightState.On -- To next state
end,
afterAsync = function()
print("Light is now On")
end,
}
},
},
[Event.SwitchOff] = {
canBeFinal = true,
from = {
[LightState.On] = {
beforeAsync = function()
print("Light is transitioning to Off")
return LightState.Off
end,
}
},
},
})
light:handle(Event.SwitchOff) -- prints "Light is transitioning to Off"
light:handle(Event.SwitchOn) -- prints "Light is transitioning to On", increases brightness over time, and then prints "Light is now On"
light:handle(Event.SwitchOn) -- warns "Illegal event `SwitchOn` called during state `On`" with a stack trace
If your project is set up to build with Rojo, the preferred installation method is using Wally. Add this to your wally.toml
file:
> StateQ = "busycityguy/[email protected]"
If you're not using Wally, you can add this repository as a submodule of your project by running the following command:
> git submodule add <https://github.com/BusyCityGuy/finite-state-machine-luau> path/to/your/dependencies
If you want to avoid submodules too, you can download the .zip
file from the latest release page.
If you aren't using Rojo, you can download the .rbxm
file from the latest release page and drag it into Roblox Studio, placing the Packages
folder in ReplicatedStorage
.
If you have other questions, bugs, feature requests, or feedback, please open an issue!
See the Contributing readme.