Zum Hauptinhalt springen

State Machines

Introduction

State Machines are designed to simplify state management.

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. They provide much of the same API as a state, but distribute behavior and triggers across all of their substates.

A state machine is made up of three main components:

  • States
  • Groups
  • Triggers

State Machine

Defining a State Machine

StateMachineDefiner

When creating a State Machine inside a component, an instance of StateMachineDefiner is used.

Properties
PropertyTypeDescription
worldWorldReference to the World.
eideidThe Entity ID of the current Component
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, eid}) => {
// Define states here
},
})

StateMachineDefinition

Alternatively you can create a State Machine independent of a component.

When creating a State Machine outside a component, an instance of StateMachineDefinition is used.

Properties
PropertyTypeDescription
initialState (Required)stringName of the starting state of the state machine
states (Required)Record<string, State>A map that stores state names and their definition
groupsStateGroup[]An optional list of state groups.
const stateMachine = {
initialState: 'a'
states: {
'a': {
onExit: () => console.log('exit a'),
triggers: {
'b': [{ type: 'timeout', timeout: 1000 }],
},
},
'b': { onEnter: () => console.log('enter b') },
},
groups: [{
substates: ['a', 'b'],
listeners: [{
target: world.events.globalId,
name: ecs.input.SCREEN_TOUCH_START,
listener: (event) => console.log('touch'),
}]
}],
}

State

A state is the fundamental atomic unit of a state machine. They can be defined directly or with the fluent StateMachineDefiner API outlined above. A state machine is always in exactly one state at a time, and will transition according to user-defined triggers associated with the current state.

Properties

PropertyTypeDescription
triggers (Required)Record<string, Trigger[]>Outgoing transitions, indexed by their target state
onEnter() => voidFunction called upon entering the state
onTick() => voidFunction called every frame while in the state
onExit() => voidFunction called when exiting the state
listenersListenerParams[]Event listener parameters, automatically added on entry and removed on exit

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, eid}) => {
const foo = ecs.defineState('foo')
...
}
})

ID

StateIds are used for specifying transition destinations. Can be either a StateDefiner or the state name itself as a string.

const a = ecs.definestate('a').wait(1000, 'b')
const b = ecs.defineState('b').wait(1000, a)
tipp

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

.initial()

Mark this state to be the current state of the state machine when it is created.

ecs.defineState('myCustomState').initial()

.onEnter()

Set a callback to run when entering this state.

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

.onTick()

Set a callback to run every frame.

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

.onExit()

Set a callback to run when exiting this state.

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

.onEvent()

Call to add an EventTrigger from this state to another that can transition when a specific event is invoked.

Properties
ParameterTypeDescription
event (Required)stringThe name of the event to listen for
nextState (Required)stateIdThe state to transition to when the event occurs
argsobjectArguments used to determine transition conditions
Args
ParameterTypeDescription
targeteidThe entity expected to receive the event (defaults to the state machine owner)
where(QueuedEvent) => booleanAn optional condition to check before transitioning; if false, the transition will not occur
ecs.defineState('myCustomState').onEvent(
ecs.input.SCREEN_TOUCH_START,
'other',
{
target: world.events.globalId,
where: (event) => event.data.position.y > 0.5
}
)

.wait()

Call to add a TimeoutTrigger from this state to another that transitions after a set amount of time.

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

.onTrigger()

Call to add a CustomTrigger from this state to another that can transition at any time immediately by the user. Use ecs.defineTrigger() to create a TriggerHandle that can be manually invoked.

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

.listen()

Call to add ListenerParams to this state's set of listeners. An event listener 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(QueuedEvent) => voidThe function to call when the event is dispatched
const handleCollision = (event) => { ... }
ecs.defineState('example').listen(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)

State Groups

Defining a State Group

StateGroupDefiner

When creating a State Group inside a component, an instance of StateGroupDefiner is used.

ParameterTypeDescription
substates (Required)StateId[]The list of states that make up this group; excluding this parameter is equivalent to listing all states
const fizz = ecs.defineState('fizz')
const buzz = ecs.defineState('buzz')

const fizzBuzz = ecs.defineStateGroup([fizz, 'buzz'])
tipp

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

.onEnter()

Set a callback to run when entering this group.

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

.onTick()

Set a callback to run every frame.

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

.onExit()

Set a callback to run when exiting this group.

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

.onEvent()

Call to add an EventTrigger from any state in this group to some other state that can transition when a specific event is invoked

Properties
ParameterTypeDescription
event (Required)stringThe name of the event to listen for
nextState (Required)stateIdThe state to transition to when the event occurs
argsobjectArguments used to determine transition conditions
Args
ParameterTypeDescription
targeteidThe entity expected to receive the event (defaults to the state machine owner)
where(QueuedEvent) => booleanAn optional condition to check before transitioning; if false, the transition will not occur
ecs.defineStateGroup(['a', 'b']).onEvent(
ecs.input.SCREEN_TOUCH_START,
'other',
{
target: world.events.globalId,
where: (event) => event.data.position.y > 0.5
}
)

.wait()

Call to add a TimeoutTrigger from any state in this group to some other state that transitions after a set amount of time.

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

.onTrigger()

Call to add a CustomTrigger from any state in this group to some other state that can transition at any time immediately by the user. Use ecs.defineTrigger() to create a TriggerHandle that can be manually invoked.

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

.listen()

Call to add ListenerParams to this state's set of listeners. An event listener 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(QueuedEvent) => voidThe function to call when the event is dispatched
const handleCollision = (event) => { ... }
ecs.defineState('example').listen(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)

Triggers

There are multiple types of triggers for transitioning under various circumstances

Handle

An object that is used to define an arbitrary transition between states. It must be created via ecs.defineTrigger, and is used by onTrigger or CustomTrigger.

ParameterTypeDescription
trigger()() => voidCall this function to cause any active CustomTrigger transitions to occur
const go = ecs.defineTrigger()
const stopped = ecs.defineState('stoppe').onTick(() => {
if (world.input.getAction('start-going')) {
go.trigger()
}
}).onTrigger(go, 'going')
const going = ecs.defineState('going')

Types

EventTrigger

EventTriggers are used to optionally transition when a specified event is invoked. The event data can be used to make a runtime decision whether to transition or not.

ParameterTypeDescription
type (Required)'event'A constant to indicate the type of the trigger
event (Required)stringThe name of the event to listen for
targeteidThe entity that is expected to receive an event
where(QueuedEvent) => booleanAn optional predicate to check before transitioning; returning false will prevent the transition from occurring
const example = {
triggers:
'other': [
{
type: 'event',
event: ecs.input.SCREEN_TOUCH_START,
target: world.events.globalId
where: (event) => event.data.position.y > 0.5
},
]
}

TimeoutTrigger

TimeoutTriggers are used to cause a transition to occur after a fixed amount of time from entering a state or group.

ParameterTypeDescription
type (Required)'timeout'A constant to indicate the type of the trigger
timeout (Required)numberThe number of milliseconds to wait before transitioning
const example = {
triggers:
'other': [
{
type: 'timeout',
timeout: 1000,
},
]
}

CustomTrigger

CustomTriggers are transitions that can be triggered at any time, causing an immediate transition. Use ecs.defineTrigger() to create a TriggerHandle that can be manually invoked.

ParameterTypeDescription
type (Required)'custom'A constant to indicate the type of the trigger
timeout (Required)numberThe number of milliseconds to wait before transitioning
const toOther = ecs.defineTrigger()
const example = {
triggers:
'other': [
{
type: 'custom',
trigger: toOther,
},
]
}
...
toOther.trigger()