Skip to content

State Machine

StateMachine registers named State instances, selects one state at a time, and forwards each frame’s update and render calls to the selected state. Game owns the shared state machine used by application states.

Register states before selecting one. A transition first exits the current state, then enters the next one with its transition data. Game.addState() can construct and register a state when you are using Game.

const stateMachine = new StateMachine();
stateMachine.add('home', new HomeState(game));
stateMachine.add('dialog', new DialogState(game));
stateMachine.change('home');
stateMachine.change('dialog', { message: 'Saved' });
sequenceDiagram
	participant Code as Application code
	participant Machine as StateMachine
	participant Current as Current State
	participant Next as Next State
	Code->>Machine: change("dialog", props)
	Machine->>Current: exit()
	Machine->>Next: enter(props)
	Machine->>Machine: currentState = next state
	loop Every frame
		Machine->>Next: update(dt)
		Machine->>Next: render(context)
	end
/** @type {Map<string, State<unknown>>} */
states;

A Map from transition names to registered state instances. Use add() to register states instead of changing this map directly.

/** @type {State<unknown> | undefined} */
currentState;

The selected state, or undefined before the first successful change() call.

Creates an empty state machine with no selected state.

stateMachine.add(stateName, state);

Registers a State instance under a string transition name. Names are usually lowercase kebab case, such as "main-menu" or "game-over".

stateMachine.add('settings', new SettingsState(game));

Register every state before transitioning to it. Adding a duplicate name replaces the previously registered state because Map.set() replaces an existing value.

stateMachine.change(stateName, stateContext);

Selects a registered state. If a state is already selected, the machine calls its exit() first. It then finds stateName, stores that state as currentState, and calls enter(stateContext) on it.

stateMachine.change('dialog', {
message: 'Your changes were saved.',
});

Use an object for transition data. The default is {}, which is suitable for a state that expects no props.

stateMachine.update(dt);

Forwards the elapsed time in seconds to currentState.update(dt). See delta time for how this makes movement and animations frame-rate independent.

stateMachine.render(context);

Forwards the drawing context to currentState.render(context). Game.render() clears the canvas before asking the state machine to render.

A State can have a different props type from every other state:

/** @typedef {{ message: string }} DialogProps */
/** @extends {State<DialogProps>} */
class DialogState extends State {}
/** @typedef {{ userId: number }} ProfileProps */
/** @extends {State<ProfileProps>} */
class ProfileState extends State {}

One StateMachine can store both instances. Its map is therefore annotated as Map<string, State<unknown>>:

/** @type {Map<string, State<unknown>>} */
states = new Map();

Read State<unknown> as “a state whose props shape is not known at this point in time.” unknown is safer than any: code cannot use an unknown value as a specific shape without checking or narrowing it first.

Syntax Meaning
Map<string, State<unknown>> A map with string keys and state-instance values whose individual props types vary.
State<unknown> | undefined Either a state or no selected state yet. | means “or.”
@param {unknown} [stateContext={}] An optional transition-data parameter whose default value is {}. The square brackets make the parameter optional.
@returns {void} The method returns no value.

The JSDoc types are checked only by tools configured to check JavaScript. In other words, they do not add runtime validation.