StateManager
This was deprecated in v1.0.0
Deprecated
StateManager is no longer maintained and is kept only for existing projects. There is no direct replacement in this library.
A small finite state machine. You build named State objects, give each one
OnEnter / OnExit callbacks, and hand them to a StateManager which keeps exactly one of them
current. Each State carries a Trove that is
cleaned every time the state is exited, so anything you connect or create while a state is
active is torn down automatically when you leave it.
The module returns a table of functions: new, newState, LoadStates, MatchesName and
IsChildOf. Everything else is a method on the StateManager or State objects.
local RunService = game:GetService("RunService")
local StateManager = require(path.to.StateManager)
local Lobby = StateManager.newState("Lobby")
Lobby:OnEnter(function(manager, trove, lastState)
print("Entered Lobby from", lastState) -- lastState is nil the first time
trove:Add(task.delay(10, function()
manager:ChangeState("Round")
end))
end)
local Round = StateManager.newState("Round")
Round:OnEnter(function(manager, trove)
trove:Connect(RunService.Heartbeat, function(dt)
-- runs only while Round is the current state
end)
end)
Round:OnExit(function()
print("Round over")
end)
local manager = StateManager.new({
States = { Lobby, Round },
DefaultState = "Lobby",
})
manager.StateChanged:Connect(function(name)
print("Current state is now", name)
end)
Depends on Sleitnick's Trove and Signal, which Wally installs alongside it.
Credits: Trove and Signal are by sleitnick (sleitnick's RbxUtil). Wally installs both.
Installation and guide: StateManager package page.
Types
StateManagerConfig
interface StateManagerConfig {States: {State}--
The states this manager can switch between; each one's Trove is added to the manager's Trove.
DefaultState: string?--
Name of a state in States to enter as soon as the manager is created.
}Configuration passed to StateManager.new.
Properties
CurrentState
This item is read only and cannot be modified. Read OnlyStateManager.CurrentState: State?The state that is currently active, or nil after ExitCurrent or before the default state is entered.
Trove
StateManager.Trove: TroveHolds every state's Trove and the StateChanged signal; Destroy destroys it.
States
This item is read only and cannot be modified. Read OnlyThe list of states passed in the config.
StateChanged
This item is read only and cannot be modified. Read OnlyStateManager.StateChanged: Signal<string?>Fires (deferred) with the new state's name whenever ChangeState switches state, just before that state's Entered signal.
Functions
newState
StateManager.newState(name: string--
The state's name, used by ChangeState and GetState.
) → State--
The new state.
Creates a new State called name with fresh Entered / Exited signals and an
empty Trove. Names should be unique within one StateManager because GetState returns the
first match.
new
Creates a StateManager over the given states. Each state's Trove is added to the manager's
Trove so Destroy cleans them all. If DefaultState is set, ChangeState(DefaultState) is
called immediately in a new thread (task.spawn), so a wrong name errors in that thread rather
than in the caller.
Errors
| Type | Description |
|---|---|
| "Config must be a table" | `Config` is not a table. |
| "States field must be a table" | `Config.States` is not a table. |
| "DefaultState must be a string" | `Config.DefaultState` is set but not a string. |
LoadStates
StateManager.LoadStates(predicate: ((ModuleScript) → boolean)?--
Optional filter; return false to skip a module. See MatchesName and IsChildOf.
) → {State}--
The states returned by the modules that loaded successfully.
Requires every ModuleScript under parent (all descendants, unless predicate rejects
them) and collects what they return as a list of States for StateManager.new. Each module is
expected to return a State made with newState. A module that errors while being required is
skipped and the error is reported in a separate thread, so one broken state does not stop the
others from loading. Requiring modules can yield if they do.
local states = StateManager.LoadStates(script.States, StateManager.IsChildOf(script.States))
local manager = StateManager.new({ States = states, DefaultState = "Lobby" })
MatchesName
StateManager.MatchesName(name: string--
A Lua string pattern to match against ModuleScript.Name.
) → (ModuleScript) → boolean--
The predicate.
Builds a predicate for LoadStates that accepts ModuleScripts whose Name matches the Lua
string pattern name (via string.match), e.g. MatchesName("State$").
IsChildOf
Builds a predicate for LoadStates that accepts only ModuleScripts that are direct children of
parent, which limits LoadStates to one level instead of all descendants.
GetState
Returns the first state in States whose Name equals name. Errors if there is none, so use
it only with names you know exist.
Errors
| Type | Description |
|---|---|
| State: "<name>" doesn't exist | No state in `States` has that name. |
ExitCurrent
StateManager:ExitCurrent() → string?--
The name of the state that was exited, or nil if there was none.
Leaves the current state without entering another one: CurrentState becomes nil, the old
state's Trove is cleaned and its Exited signal fires synchronously. StateChanged does not
fire. Does nothing (and returns nil) when there is no current state.
ChangeState
StateManager:ChangeState(stateName: string--
Name of the state to enter.
) → ()Switches to the state called stateName. In order:
- Errors if that state is already current.
- Calls
ExitCurrent(cleans the old state's Trove and fires itsExitedsynchronously). -
Looks the new state up with
GetState(errors if it does not exist; the old state has already been exited at this point, leavingCurrentStatenil). -
Sets
CurrentState, then on the nexttask.deferstep firesStateChanged(stateName)followed by the new state'sEntered(self, state.Trove, lastStateName).
Because the enter callbacks are deferred, code right after ChangeState runs before any
OnEnter callback does.
Errors
| Type | Description |
|---|---|
| "<stateName> is already the current state" | The named state is already current. |
| State: "<stateName>" doesn't exist | No state in `States` has that name. |
Destroy
StateManager:Destroy() → ()
Destroys the manager's Trove, which cleans every state's Trove and destroys the
StateChanged signal. Exited signals are not fired and CurrentState is left as is; call
ExitCurrent first if you want exit callbacks to run.