Skip to main content

StateManager

This was deprecated in v1.0.0
No longer maintained; kept for existing projects. There is no direct replacement.
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 Only
StateManager.CurrentState: State?

The state that is currently active, or nil after ExitCurrent or before the default state is entered.

Trove​

StateManager.Trove: Trove

Holds every state's Trove and the StateChanged signal; Destroy destroys it.

States​

This item is read only and cannot be modified. Read Only
StateManager.States: {State}

The list of states passed in the config.

StateChanged​

This item is read only and cannot be modified. Read Only
StateManager.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​

StateManager.new(
Config: StateManagerConfig--

The states and optional default state.

) → StateManager--

The new manager.

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

TypeDescription
"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(
parent: Instance,--

The instance whose descendant ModuleScripts are loaded.

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​

StateManager.IsChildOf(
parent: Instance--

The required parent.

) → (ModuleScript) → boolean--

The predicate.

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​

StateManager:GetState(
name: string--

The state's name.

) → State--

The matching state.

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

TypeDescription
State: "<name>" doesn't existNo 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:

  1. Errors if that state is already current.
  2. Calls ExitCurrent (cleans the old state's Trove and fires its Exited synchronously).
  3. Looks the new state up with GetState (errors if it does not exist; the old state has already been exited at this point, leaving CurrentState nil).
  4. Sets CurrentState, then on the next task.defer step fires StateChanged(stateName) followed by the new state's Entered(self, state.Trove, lastStateName).

Because the enter callbacks are deferred, code right after ChangeState runs before any OnEnter callback does.

Errors

TypeDescription
"<stateName> is already the current state"The named state is already current.
State: "<stateName>" doesn't existNo 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.

Show raw api
{
    "functions": [
        {
            "name": "newState",
            "desc": "Creates a new [State](/api/State) called `name` with fresh `Entered` / `Exited` signals and an\nempty `Trove`. Names should be unique within one StateManager because `GetState` returns the\nfirst match.",
            "params": [
                {
                    "name": "name",
                    "desc": "The state's name, used by `ChangeState` and `GetState`.",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "The new state.",
                    "lua_type": "State"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 143,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "new",
            "desc": "Creates a StateManager over the given states. Each state's `Trove` is added to the manager's\nTrove so `Destroy` cleans them all. If `DefaultState` is set, `ChangeState(DefaultState)` is\ncalled immediately in a new thread (`task.spawn`), so a wrong name errors in that thread rather\nthan in the caller.",
            "params": [
                {
                    "name": "Config",
                    "desc": "The states and optional default state.",
                    "lua_type": "StateManagerConfig"
                }
            ],
            "returns": [
                {
                    "desc": "The new manager.",
                    "lua_type": "StateManager"
                }
            ],
            "function_type": "static",
            "errors": [
                {
                    "lua_type": "\"Config must be a table\"",
                    "desc": "`Config` is not a table."
                },
                {
                    "lua_type": "\"States field must be a table\"",
                    "desc": "`Config.States` is not a table."
                },
                {
                    "lua_type": "\"DefaultState must be a string\"",
                    "desc": "`Config.DefaultState` is set but not a string."
                }
            ],
            "source": {
                "line": 255,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "GetState",
            "desc": "Returns the first state in `States` whose `Name` equals `name`. Errors if there is none, so use\nit only with names you know exist.",
            "params": [
                {
                    "name": "name",
                    "desc": "The state's name.",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "The matching state.",
                    "lua_type": "State"
                }
            ],
            "function_type": "method",
            "errors": [
                {
                    "lua_type": "State: \"<name>\" doesn't exist",
                    "desc": "No state in `States` has that name."
                }
            ],
            "source": {
                "line": 301,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "ExitCurrent",
            "desc": "Leaves the current state without entering another one: `CurrentState` becomes `nil`, the old\nstate's `Trove` is cleaned and its `Exited` signal fires synchronously. `StateChanged` does not\nfire. Does nothing (and returns `nil`) when there is no current state.",
            "params": [],
            "returns": [
                {
                    "desc": "The name of the state that was exited, or nil if there was none.",
                    "lua_type": "string?"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 320,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "ChangeState",
            "desc": "Switches to the state called `stateName`. In order:\n\n1. Errors if that state is already current.\n2. Calls `ExitCurrent` (cleans the old state's Trove and fires its `Exited` synchronously).\n3. Looks the new state up with `GetState` (errors if it does not exist; the old state has\n   already been exited at this point, leaving `CurrentState` nil).\n4. Sets `CurrentState`, then on the next `task.defer` step fires `StateChanged(stateName)`\n   followed by the new state's `Entered(self, state.Trove, lastStateName)`.\n\nBecause the enter callbacks are deferred, code right after `ChangeState` runs before any\n`OnEnter` callback does.",
            "params": [
                {
                    "name": "stateName",
                    "desc": "Name of the state to enter.",
                    "lua_type": "string"
                }
            ],
            "returns": [],
            "function_type": "method",
            "errors": [
                {
                    "lua_type": "\"<stateName> is already the current state\"",
                    "desc": "The named state is already current."
                },
                {
                    "lua_type": "State: \"<stateName>\" doesn't exist",
                    "desc": "No state in `States` has that name."
                }
            ],
            "source": {
                "line": 359,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "Destroy",
            "desc": "Destroys the manager's Trove, which cleans every state's `Trove` and destroys the\n`StateChanged` signal. `Exited` signals are not fired and `CurrentState` is left as is; call\n`ExitCurrent` first if you want exit callbacks to run.",
            "params": [],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 383,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "LoadStates",
            "desc": "Requires every `ModuleScript` under `parent` (all descendants, unless `predicate` rejects\nthem) and collects what they return as a list of States for `StateManager.new`. Each module is\nexpected to return a State made with `newState`. A module that errors while being required is\nskipped and the error is reported in a separate thread, so one broken state does not stop the\nothers from loading. Requiring modules can yield if they do.\n\n```lua\nlocal states = StateManager.LoadStates(script.States, StateManager.IsChildOf(script.States))\nlocal manager = StateManager.new({ States = states, DefaultState = \"Lobby\" })\n```",
            "params": [
                {
                    "name": "parent",
                    "desc": "The instance whose descendant ModuleScripts are loaded.",
                    "lua_type": "Instance"
                },
                {
                    "name": "predicate",
                    "desc": "Optional filter; return `false` to skip a module. See `MatchesName` and `IsChildOf`.",
                    "lua_type": "((ModuleScript) -> boolean)?"
                }
            ],
            "returns": [
                {
                    "desc": "The states returned by the modules that loaded successfully.",
                    "lua_type": "{State}"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 406,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "MatchesName",
            "desc": "Builds a predicate for `LoadStates` that accepts ModuleScripts whose `Name` matches the Lua\nstring pattern `name` (via `string.match`), e.g. `MatchesName(\"State$\")`.",
            "params": [
                {
                    "name": "name",
                    "desc": "A Lua string pattern to match against `ModuleScript.Name`.",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "The predicate.",
                    "lua_type": "(ModuleScript) -> boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 443,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "IsChildOf",
            "desc": "Builds a predicate for `LoadStates` that accepts only ModuleScripts that are direct children of\n`parent`, which limits `LoadStates` to one level instead of all descendants.",
            "params": [
                {
                    "name": "parent",
                    "desc": "The required parent.",
                    "lua_type": "Instance"
                }
            ],
            "returns": [
                {
                    "desc": "The predicate.",
                    "lua_type": "(ModuleScript) -> boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 458,
                "path": "packages/src/StateManager/init.luau"
            }
        }
    ],
    "properties": [
        {
            "name": "CurrentState",
            "desc": "The state that is currently active, or `nil` after `ExitCurrent` or before the default state is entered.",
            "lua_type": "State?",
            "readonly": true,
            "source": {
                "line": 202,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "Trove",
            "desc": "Holds every state's `Trove` and the `StateChanged` signal; `Destroy` destroys it.",
            "lua_type": "Trove",
            "source": {
                "line": 207,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "States",
            "desc": "The list of states passed in the config.",
            "lua_type": "{State}",
            "readonly": true,
            "source": {
                "line": 213,
                "path": "packages/src/StateManager/init.luau"
            }
        },
        {
            "name": "StateChanged",
            "desc": "Fires (deferred) with the new state's name whenever `ChangeState` switches state, just before that state's `Entered` signal.",
            "lua_type": "Signal<string?>",
            "readonly": true,
            "source": {
                "line": 219,
                "path": "packages/src/StateManager/init.luau"
            }
        }
    ],
    "types": [
        {
            "name": "StateManagerConfig",
            "desc": "Configuration passed to `StateManager.new`.",
            "fields": [
                {
                    "name": "States",
                    "lua_type": "{State}",
                    "desc": "The states this manager can switch between; each one's `Trove` is added to the manager's Trove."
                },
                {
                    "name": "DefaultState",
                    "lua_type": "string?",
                    "desc": "Name of a state in `States` to enter as soon as the manager is created."
                }
            ],
            "source": {
                "line": 236,
                "path": "packages/src/StateManager/init.luau"
            }
        }
    ],
    "name": "StateManager",
    "desc": ":::caution Deprecated\nStateManager is no longer maintained and is kept only for existing projects. There is no\ndirect replacement in this library.\n:::\n\nA small finite state machine. You build named [State](/api/State) objects, give each one\n`OnEnter` / `OnExit` callbacks, and hand them to a StateManager which keeps exactly one of them\ncurrent. Each State carries a [Trove](https://sleitnick.github.io/RbxUtil/api/Trove/) that is\ncleaned every time the state is exited, so anything you connect or create while a state is\nactive is torn down automatically when you leave it.\n\nThe module returns a table of functions: `new`, `newState`, `LoadStates`, `MatchesName` and\n`IsChildOf`. Everything else is a method on the `StateManager` or `State` objects.\n\n```lua\nlocal RunService = game:GetService(\"RunService\")\nlocal StateManager = require(path.to.StateManager)\n\nlocal Lobby = StateManager.newState(\"Lobby\")\nLobby:OnEnter(function(manager, trove, lastState)\n\tprint(\"Entered Lobby from\", lastState) -- lastState is nil the first time\n\ttrove:Add(task.delay(10, function()\n\t\tmanager:ChangeState(\"Round\")\n\tend))\nend)\n\nlocal Round = StateManager.newState(\"Round\")\nRound:OnEnter(function(manager, trove)\n\ttrove:Connect(RunService.Heartbeat, function(dt)\n\t\t-- runs only while Round is the current state\n\tend)\nend)\nRound:OnExit(function()\n\tprint(\"Round over\")\nend)\n\nlocal manager = StateManager.new({\n\tStates = { Lobby, Round },\n\tDefaultState = \"Lobby\",\n})\n\nmanager.StateChanged:Connect(function(name)\n\tprint(\"Current state is now\", name)\nend)\n```\n\nDepends on Sleitnick's [Trove](https://sleitnick.github.io/RbxUtil/api/Trove/) and\n[Signal](https://sleitnick.github.io/RbxUtil/api/Signal/), which Wally installs alongside it.\n\n**Credits:** [Trove](https://sleitnick.github.io/RbxUtil/api/Trove/) and [Signal](https://sleitnick.github.io/RbxUtil/api/Signal/) are by [sleitnick](https://github.com/Sleitnick) ([sleitnick's RbxUtil](https://sleitnick.github.io/RbxUtil/)). Wally installs both.\n\nInstallation and guide: [StateManager package page](/docs/packages/state-manager).",
    "deprecated": {
        "version": "v1.0.0",
        "desc": "No longer maintained; kept for existing projects. There is no direct replacement."
    },
    "source": {
        "line": 60,
        "path": "packages/src/StateManager/init.luau"
    }
}