State Machine
State machines are one of the basic tools for organizing your code. States allow you to switch between different behaviors, values, and setup/teardown logic based on user-defined conditions. A state machine is made up of three main components:
- States
- Triggers
- Groups
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.
You can add a state machine to your custom component using the stateMachine
property in the component's definition. It must be a StateMachineDefiner
or StateMachineDefinition
. These state machines exist on each entity with the component, and will be automatically created and destroyed when the object is added to and removed from the world.
ecs.registerComponent({
...
stateMachine: ({world, eid}) => {
// Define states here
},
})
You can also create and manage an independent state machine by calling the ecs.createStateMachine
function directly:
const machineId = ecs.createMachine(world, eid, machineDef)
...
ecs.deleteStateMachine(world, machineId)
A state machine is a combination of states and the logic that transitions between those states. They are typically defined within a StateMachineDefiner
function that is called when the machine is created. Here is an example of a state machine to move an object back and forth until it hits something.
const BackAndForth = ecs.registerComponent({
name: 'back-and-forth',
schema: {
forthX: ecs.f32,
forthY: ecs.f32,
forthZ: ecs.f32,
duration: ecs.f32,
},
stateMachine: ({world, eid, schemaAttribute}) => {
const position = {...ecs.Position.get(world, eid)}
const backX = position.x
const backY = position.y
const backZ = position.z
const {forthX, forthY, forthZ, duration} = schemaAttribute.get(eid)
const waitBeforeBack = ecs.defineState('waitBeforeBack').wait(1000, 'back')
const back = ecs.defineState('back').initial()
.onEvent('position-animation-complete', 'waitBeforeForth')
.onEnter(() => {
ecs.PositionAnimation.set(world, eid, {
autoFrom: true
toX: forthX,
toY: forthY,
toZ: forthZ,
duration,
})
})
const waitbeforeForth = ecs.defineState('waitBeforeForth').wait(1000, 'forth')
const forth = ecs.defineState('forth')
.onEvent('position-animation-complete', waitBeforeBack)
.onEnter(() => {
ecs.PositionAnimation.set(world, eid, {
autoFrom: true
toX: backX,
toY: backY,
toZ: backZ,
duration,
})
})
const stopped = ecs.defineState('stopped').onEnter(() => {
ecs.PositionAnimation.remove(world, eid)
})
ecs.defineStateGroup([back, forth]).onEvent(ecs.Physics.COLLISION_START_EVENT, stopped)
},
})
Here is another example of a machine of changing a character state:
const Character = ecs.registerComponent({
name: 'character',
schema: {
walkingSpeed: ecs.f32,
runningSpeed: ecs.f32,
staminaRate: ecs.f32,
jumpForce: ecs,f32,
},
stateMachine: ({world, eid, schemaAttribute}) => {
const idle = ecs.defineState('idle').initial()
const walking = ecs.defineState('walking')
const running = ecs.defineState('running')
const airborne = ecs.defineState('airborne')
const moving = ecs.defineStateGroup([walking, running])
const canJump = ecs.defineStateGroup([idle, walking, running])
const recovering = ecs.defineStateGroup([idle, walking])
const all = ecs.defineStateGroup()
const startMoving = ecs.defineTrigger()
const stopMoving = ecs.defineTrigger()
idle.onTrigger(startMoving, walking)
.onTick(() => {
if (world.input.getAction('forward')) {
startMoving.toggle()
}
})
moving.onTrigger(stopMoving, idle)
.onTick((currentState) => {
const {walkingSpeed, runningSpeed} = schemaAttrbiute.get(eid)
let speed = currentState === 'running' ? runningSpeed : walkingSpeed
ecs.physics.applyForce(world, eid, 0, 0, speed)
if (!world.input.getAction('forward')) {
stopMoving.toggle()
}
})
walking.onEvent('toggle-sprint', running)
running.onEvent('toggle-sprint', walking)
canJump.onEvent('jump', airborne)
airborne.onEnter(() => ecs.physics.applyForce(world, eid, 0, schemaAttribute.get(eid).jumpForce, 0))
.onEvent(ecs.Physics.COLLISION_START_EVENT, walking, {where: () => world.input.getAction('forward')})
.onEvent(ecs.Physics.COLLISION_START_EVENT, idle)
let stamina = 1.0
const updateStamina = (add: boolean) => {
const diff = schemaAttribute.get(eid).staminaRate * world.time.delta
stamina += add ? diff : -diff
}
running.onTick(() => updateStamina(false))
recovering.onTick(() => updateStamina(true))
all.listen(eid, 'powerup-collected', () => stamina = 1.0)
},
})
StateMachineDefiner
A StateMachineDefiner
is a function that creates a state machine and all of its parts. Inside the function you define states and groups, as well as locally scoped variables and functions to use in them. It can be passed into a component's stateMachine
prop and will be called when an entity is added to the world. Alternatively you can directly pass a StateMachineDefinition
, which is outlined below.
The StateMachineDefiner
function accepts an object with the following properties:
Key | Type | Description |
---|---|---|
world | World | The world the machine is created in |
eid | Eid | The entity owner of the machine |
schemaAttribute | WorldAttribute | Access to the component's schema |
dataAttribute | WorldAttribute | Access to the component's data |
defineState()
Use this function to create a State
from within a StateMachineDefiner
Parameter | Type | Description |
---|---|---|
name | string | The unique name of the state |
Returns a StateDefiner
Example
stateMachine: ({world, eid}) => {
const foo = ecs.defineState('foo')
...
}
StateDefiner
StateDefiner functions are 'fluent', so they all return the instance itself. This means you can chain any of the following consecutively in a single statement.
.initial()
Mark this state to be the current state of the state machine when it is created.
Example
ecs.defineState('name').initial()
.onEnter()
Set a callback to run when entering this state.
Parameter | Type | Description |
---|---|---|
cb | () => void | A function called when the state is entered |
Example
ecs.defineState('name').onEnter(() => {
// Do something
})
.onTick()
Set a callback to run every frame.
Parameter | Type | Description |
---|---|---|
cb | () => void | A function called once each frame while in this state |
Example
ecs.defineState('name').onTick(() => {
// Do something
})
.onExit()
Set a callback to run when exiting this state.
Parameter | Type | Description |
---|---|---|
cb | () => void | A function called when the state exits |
Example
ecs.defineState('name').onExit(() => {
// Do something
})
.onEvent()
Call to add an EventTrigger
from this state to another that can transition when a specific event is invoked
Parameter | Type | Description |
---|---|---|
event | string | The event to listen for |
nextState | StateId | The next state to transition to |
args | object (see below) | Optional arguments for determining when to transition |
args
Key | Type | Required | Description |
---|---|---|---|
target | Eid | no | The entity that is expected to receive an event (defaults to the state machine owner) |
where | (QueuedEvent) => boolean | no | An optional predicate to check before transitioning; returning false will prevent the transition from occurring |
Example
ecs.defineState('example').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.
Parameter | Type | Description |
---|---|---|
timeout | number | The duration in milliseconds before transitioning |
nextState | StateId | The next state to transition to |
Example
ecs.defineState('example').wait(1000, 'other')
.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.
Parameter | Type | Description |
---|---|---|
handle | TriggerHandle | The handle that will cause a transition when manually activated |
nextState | StateId | The next state to transition to |
Example
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.
Parameter | Type | Description |
---|---|---|
target | Eid or () => Eid | The entity that is expected to receive an event |
name | string | The event to listen for |
listener | (QueuedEvent) => void | The function to call when the event is dispatched |
Example
const handleCollision = (event) => { ... }
ecs.defineState('example').listen(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
defineStateGroup()
Use this function to create a StateGroup
from within a StateMachineDefiner
.
Parameter | Type | Required | Description |
---|---|---|---|
substates | StateId[] | no | The list of states that make up this group; excluding this parameter is equivalent to listing all states |
Returns a StateGroupDefiner
Example
const foo = ecs.defineState('foo')
const fizz = ecs.defineState('fizz')
const buzz = ecs.defineState('buzz')
const fizzBuzz = ecs.defineStateGroup([fizz, 'buzz'])
StateGroupDefiner
StateGroupDefiner
functions are 'fluent', so they all return the instance itself. This means you can chain any of the following consecutively in a single statement.
.onEnter()
Set a callback to run when entering this group from outside it.
Parameter | Type | Description |
---|---|---|
cb | (string) => void | A function called when a substate is entered from a non-substate, optionally providing the specific state that was entered |
Example
ecs.defineStateGroup(['a', 'b']).onEnter(() => {
// Do something
})
.onTick()
Set a callback to run every frame.
Parameter | Type | Description |
---|---|---|
cb | (string) => void | A function called once each frame while in any substate, optionally providing the current state |
Example
ecs.defineStateGroup(['a', 'b']).onTick(() => {
// Do something
})
.onExit()
Set a callback to run when exiting this state.
Parameter | Type | Description |
---|---|---|
cb | (string) => void | A function called when a substate exits to a non-substate, optionally providing the specific state that was exited |
Example
ecs.defineStateGroup(['a', 'b']).onExit(() => {
// 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
Parameter | Type | Description |
---|---|---|
event | string | The event to listen for |
nextState | StateId | The state to transition to |
args | object (see below) | optional arguments for determining when to transition |
args
Key | Type | Required | Description |
---|---|---|---|
target | Eid | no | The entity that is expected to receive an event |
where | (QueuedEvent) => boolean | no | An optional predicate to check before transitioning; returning false will prevent the transition from occurring |
Example
ecs.defineStateGroup(['a', 'b']).onEvent('click', 'c')
.wait()
Call to add a TimeoutTrigger
from any state in this group to some other state that transitions after a set amount of time.
Parameter | Type | Description |
---|---|---|
timeout | number | The duration in milliseconds before transitioning |
nextState | StateId | The state to transition to |
Example
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.
Parameter | Type | Description |
---|---|---|
handle | TriggerHandle | The handle that will cause a transition when manually activated |
nextState | StateId | The state to transition to |
Example
const toC = ecs.defineTrigger()
ecs.defineStateGroup(['a', 'b']).onTrigger(toC, 'c')
...
toC.trigger()
.listen()
Call to add ListenerParams
to this group's set of listeners. An event listener will be automatically added when the group is entered, and removed on exit.
Parameter | Type | Description |
---|---|---|
target | Eid or () => Eid | The entity that is expected to receive an event |
name | string | The event to listen for |
listener | (QueuedEvent) => void | The function to call when the event is dispatched |
Example
const handleCollision = (event) => { ... }
ecs.defineStateGroup(['a', 'b']).listen(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
StateMachineDefinition
Instead of using the StateMachineDefiner
to define states and transitions, you can pass a StateMachineDefinition
, which is an object containing all of the states and their transitions.
Key | Type | Required | Description |
---|---|---|---|
initialState | string | yes | Name of the starting state of the state machine |
states |
| yes | A map that stores state names and their definition |
groups | StateGroup[] | no | An optional list of state groups |
Example
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.
Key | Type | Required | Description |
---|---|---|---|
triggers |
| yes | All outgoing transitions indexed by their destination state |
onEnter | () => void | no | A function called when the state is entered |
onTick | () => void | no | A function called once each frame while in this state |
onExit | () => void | no | A function called when the state exits |
listeners | ListenerParams[] | no | Parameters for event listeners that will be automatically added and removed when entering and exiting the state |
Example:
const forth = {
triggers: {
'waitBeforeBack': {
type: 'event',
event: 'animation-complete',
},
},
onEnter: () => {
ecs.PositionAnimation.set(world, eid, {
autoFrom: true
toX: backX,
toY: backY,
toZ: backZ,
duration,
})
},
},
StateGroup
A state group is an abstract wrapper around multiple states of a state machine. It is a useful tool for organizing code, preventing repetitive statements, and simplifying logic. It is not a state itself and thus cannot be transitioned to. It can however have outgoing transitions, which are essentially shorthand for defining those same outgoing transitions individually on all substates. Its onEnter
and onExit
callbacks will not be invoked when transitioning between its substates.
Key | Type | Required | Description |
---|---|---|---|
substates | StateId[] | no | The list of states that make up this group; excluding this parameter is equivalent to listing all states |
triggers |
| yes | All outgoing transitions indexed by their destination state |
onEnter | () => void | no | A function called when a substate is entered from a non-substate |
onTick | () => void | no | A function called once each frame while in any substate |
onExit | () => void | no | A function called when a substate exits to a non-substate |
listeners | ListenerParams[] | no | Parameters for event listeners that will be automatically added and removed when entering and exiting the group |
Example:
const forth = {
triggers: {
'waitBeforeBack': {
type: 'event',
event: 'animation-complete',
},
},
onEnter: () => {
ecs.PositionAnimation.set(world, eid, {
autoFrom: true
toX: backX,
toY: backY,
toZ: backZ,
duration,
})
},
},
Trigger
There are multiple types of triggers for transitioning under various circumstances
EventTrigger
EventTrigger
s 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.
Key | Type | Required | Description |
---|---|---|---|
type | 'event' | yes | A constant to indicate the type of the trigger |
event | string | yes | The name of the event to listen for |
target | Eid | no | The entity that is expected to receive an event |
where | (QueuedEvent) => boolean | no | An optional predicate to check before transitioning; returning false will prevent the transition from occurring |
Example:
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
TimeoutTrigger
s are used to cause a transition to occur after a fixed amount of time from entering a state or group.
Key | Type | Required | Description |
---|---|---|---|
type | 'timeout' | yes | A constant to indicate the type of the trigger |
timeout | number | yes | The number of milliseconds to wait before transitioning |
Example:
const example = {
triggers:
'other': [
{
type: 'timeout',
timeout: 1000,
},
]
}
CustomTrigger
CustomTrigger
s 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.
Key | Type | Required | Description |
---|---|---|---|
type | 'custom' | yes | A constant to indicate the type of the trigger |
handle | TriggerHandle | yes | The handle that will cause a transition when manually activated |
Example:
const toOther = ecs.defineTrigger()
const example = {
triggers:
'other': [
{
type: 'custom',
trigger: toOther,
},
]
}
...
toOther.trigger()
StateId
StateId
s 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)
ListenerParams
A ListenerParams
object is a type that is used for storing the parameters needed to supply an event listener. They are passed into a state via the listeners
property of a State
or StateGroup
object, or created automatically when calling listen()
on a StateDefiner
or StateGroupDefiner
.
Key | Type | Required | Description |
---|---|---|---|
target | Eid or () => Eid | yes | The entity that is expected to receive an event |
name | string | yes | The event to listen for |
listener | (QueuedEvent) => void | yes | The function to call when the event is dispatched |
TriggerHandle
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
.
Key | Type | Description |
---|---|---|
trigger() | () => void | Call this function to cause any active CustomTrigger transitions to occur |
Example
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')