Skip to main content

State Machines

Introduction

State Machines are designed to simplify state management.

A state machine is made up of three main components:

  • States
  • State Groups
  • Triggers

A state machine is always in exactly one state at a time, and will transition between states when certain conditions (defined by the triggers) are met. State groups are a convenient way to bundle shared logic between multiple states, but the groups are not states themselves.

Example

ecs.registerComponent({
name: 'Jump On Touch',
stateMachine: ({world, entity, defineState}) => {
const idle = defineState('idle').initial().onEnter(() => {
console.log('Entering idle state')
}).onEvent(ecs.input.SCREEN_TOUCH_START, 'jumping')

const jumping = defineState('jumping').onEnter(() => {
console.log('Entering jumping state')
ecs.physics.applyImpulse(world, entity.eid, 0, 5, 0)
}).onTick(() => {
console.log('In jumping state')
}).wait(2000, 'idle')
},
})

Defining a State Machine

When creating a state machine inside a component, your function is called with the following:

Properties

PropertyTypeDescription
worldWorldReference to the World.
eideidThe Entity ID of the current Component
entityEntityThe Entity instance of the current Component
defineStatefunctionA function to define states on the state machine
defineStateGroupfunctionA function to define groups on the state machine
schemaAttributeWorldAttributeReference to the current Component's schema in World Scope.
dataAttributeWorldAttributeReference to the current Component's data in World Scope.

The following code is an example of how to define an empty state machine:

ecs.registerComponent({
...
stateMachine: ({world, entity, defineState}) => {
// Define states here
},
})

State

A state is the fundamental atomic unit of a state machine. After defining the possible states of your state machine, you can move between States by defining triggers.

Defining a State

The following code is an example of how to define a new State inside a state machine within a component.

ecs.registerComponent({
...
stateMachine: ({world, entity, defineState}) => {
const foo = defineState('foo')
...
}
})
tip

State functions are “fluent,” meaning they return the same instance of the State, allowing you to chain multiple function calls in a single statement.

.initial()

Mark this state as the first active state when the state machine is created.

defineState('myCustomState').initial()

.onEnter()

Set a callback to run when entering this state.

defineState('myCustomState').onEnter(() => {
// Do something
})

.onTick()

Set a callback to run every frame while this state is active.

defineState('myCustomState').onTick(() => {
// Do something
})

.onExit()

Set a callback to run when exiting this state.

defineState('myCustomState').onExit(() => {
// Do something
})

.onEvent()

Transition to a new state when a specific event is received.

ParameterTypeDescription
event (Required)stringThe name of the event to listen for
nextState (Required)string or StateThe state to transition to when the event occurs
options (Optional)objectAdditional options

Options

ParameterTypeDescription
targeteidThe entity expected to receive the event (defaults to the current entity)
where(event) => booleanAn optional condition to check before transitioning; if false, the transition will not occur
defineState('myCustomState').onEvent(
ecs.input.SCREEN_TOUCH_START,
'other',
{
target: world.events.globalId,
where: (event) => event.data.position.y > 0.5
}
)

.wait()

Transition to a new state after a set amount of time.

ParameterTypeDescription
timeoutnumberThe duration in milliseconds before transitioning
nextStatestring or StateThe next state to transition to
defineState('myCustomState').wait(1000, 'myOtherCustomState')

.onTrigger()

Transition to a new state when a TriggerHandle (defined with ecs.defineTrigger()) is triggered.

ParameterTypeDescription
handleTriggerHandleThe handle that will cause a transition when manually activated
nextStatestring or StateThe next state to transition to
const toOther = ecs.defineTrigger()
defineState('example').onTrigger(toOther, 'other')
...
toOther.trigger()

.listen()

Register an event listener that will be automatically added when the state is entered, and removed on exit.

ParameterTypeDescription
targeteid or () => eidThe entity that is expected to receive an event
namestringThe event to listen for
listener(event) => voidThe function to call when the event is dispatched
const handleCollision = (event) => { 
console.log('Collided with', event.data.other)
}
defineState('example').listen(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)

State Groups

A state group is a way to define behavior and triggers that apply to a list of states. State groups are not states themselves, and cannot be transitioned into directly. Instead, when any state in the group is active, the group’s behavior and triggers are also active.

Defining a State Group

ParameterTypeDescription
substates (Optional)Array of string or StateThe list of states that make up this group; excluding this parameter is equivalent to listing all states
const fizz = defineState('fizz')
const buzz = defineState('buzz')

const fizzBuzz = defineStateGroup([fizz, 'buzz'])
tip

State Group functions are “fluent,” meaning they return the same instance of the State Group, allowing you to chain multiple function calls in a single statement.

.onEnter()

Set a callback to run when entering this group.

defineStateGroup(['a', 'b']).onEnter(() => {
// Do something
})

.onTick()

Set a callback to run every frame while this group is active.

defineStateGroup(['a', 'b']).onTick(() => {
// Do something
})

.onExit()

Set a callback to run when exiting this group.

defineStateGroup(['a', 'b']).onTick(() => {
// Do something
})

.onEvent()

Transition to a new state when a specific event is received.

ParameterTypeDescription
event (Required)stringThe name of the event to listen for
nextState (Required)string or StateThe state to transition to when the event occurs
options (Optional)objectAdditional options

Options

ParameterTypeDescription
targeteidThe entity expected to receive the event (defaults to the current entity)
where(event) => booleanAn optional condition to check before transitioning; if false, the transition will not occur
defineStateGroup(['a', 'b']).onEvent(
ecs.input.SCREEN_TOUCH_START,
'other',
{
target: world.events.globalId,
where: (event) => event.data.position.y > 0.5
}
)

.wait()

Transition to a new state after a set amount of time.

ParameterTypeDescription
timeoutnumberThe duration in milliseconds before transitioning
nextStatestring or StateThe next state to transition to
defineStateGroup(['a', 'b']).wait(1000, 'c')

.onTrigger()

Transition to a new state when a TriggerHandle (defined with ecs.defineTrigger()) is triggered.

ParameterTypeDescription
handleTriggerHandleThe handle that will cause a transition when manually activated
nextStatestring or StateThe next state to transition to
const toC = ecs.defineTrigger()
defineStateGroup(['a', 'b']).onTrigger(toC, 'c')
...
toC.trigger()

.listen()

Register an event listener that will be automatically added when the state group is entered, and removed on exit.

ParameterTypeDescription
targeteid or () => eidThe entity that is expected to receive an event
namestringThe event to listen for
listener(event) => voidThe function to call when the event is dispatched
const handleCollision = (event) => { 
console.log('collided with', event.data.other)
}
defineStateGroup(['a', 'b']).listen(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)

Custom Triggers

You can define a custom trigger that can be invoked at any time to cause a transition.

const go = ecs.defineTrigger()
const stopped = defineState('stopped').onTick(() => {
if (world.input.getAction('start-going')) {
go.trigger()
}
}).onTrigger(go, 'going')
const going = defineState('going')