メインコンテンツへスキップ

State Machine

State machines are one of the basic tools for organizing your code. States are essentially your past, present, and future values. This implies that, depending on the circumstances, you can swap between these values.

You can add a state machine to your custom component using the stateMachine attribute on the component register.

ecs.registerComponent({
name: 'component-name',
...
stateMachine: ({world, owner}) => {
// Your state machine definition here
},
})

You can also create a state machine by calling the ecs.createStateMachine function directly:

const machine = createMachine(world, owner, machineDef)

A state machine is a combination of states and the logic that transitions between those states. You can pass a StateMachineDefinition object to define those states and transitions. Here is an example of a state machine to move an object back and forth.

import * as ecs '@8thwall/ecs'

const BackAndForth = ecs.registerComponent({
name: 'back-and-forth',
schema: {
forthX: ecs.f32,
forthY: ecs.f32,
forthZ: ecs.f32,
duration: ecs.f32,
},
schemaDefaults: {
forthX: 5,
forthY: 0,
forthZ: 0,
duration: 1000,
},
stateMachine: ({world, owner}) => {
const position = {...ecs.Position.get(world, owner)}
const backX = position.x
const backY = position.y
const backZ = position.z

const {forthX, forthY, forthZ, duration} = BackAndForth.get(world, owner)

const back = ecs.defineState('back').initial()
.onEvent('position-animation-complete', 'waitBeforeForth')
.onEnter(() => {
ecs.PositionAnimation.set(world, owner, {
fromX: backX,
fromY: backY,
fromZ: backZ,
toX: forthX,
toY: forthY,
toZ: forthZ,
duration,
})
})

const waitBeforeBack = ecs.defineState('waitBeforeBack').wait(1000, back)
const forth = ecs.defineState('forth')
.onEvent('position-animation-complete', waitBeforeBack)
.onEnter(() => {
ecs.PositionAnimation.set(world, owner, {
fromX: forthX,
fromY: forthY,
fromZ: forthZ,
toX: backX,
toY: backY,
toZ: backZ,
duration,
})
})
ecs.defineState('waitBeforeForth').wait(1000, forth)
},
})

export {BackAndForth}

Here is another example of a machine of changing a character state base on collision events:

const Dancer = ecs.registerComponent({
name: 'dancer',
schema: {
},
data: {
machineId: ecs.f32,
},
stateMachine: ({world, owner: eid}) => {
const idle = ecs.defineState('idle')
.onEvent(ecs.Physics.COLLISION_START_EVENT, 'dance')
.onEnter(() => {
ecs.GltfModel.set(world, eid, {animationClip: 'idle'})
})

ecs.defineState('dance').initial()
.wait(500, idle)
.onEnter(() => {
ecs.GltfModel.set(world, eid, {animationClip: 'hiphop'})
})
},
})

StateMachineDefinition

KeyTypeRequiredDescription
initialStatestringyesName of the starting state of the state machine.
statesRecord<string, State>yesA map that store state name and their definition.

Each state in the states field has the type State with the following definition (see below)

State

KeyTypeRequiredDescription
triggersRecord<string, Trigger[]>yesName of the next states that this state and transition to and the triggers to do so.
onEnterfunctionnoa function called before the state is entered.
onExitfunctionnoa function called before the state exits.

Example:

forth: {
triggers: {
waitBeforeBack: {
type: 'event',
event: 'animation-complete',
},
},
onEnter: () => {
console.log('entering forth')
Animation.set(world, component.id, {
attribute: 'position',
property: 'x',
from: 5,
to: -5,
duration: 1000,
loop: 0,
reverse: 0,
})
},
},

Trigger

There are two type of triggers: event and timeout

EventTrigger

KeyTypeRequiredDescription
typeconstant: 'event'yes a constant to indicate the type of the trigger
eventstringyesthe event type that trigger this
targetEidnothe entity you want this trigger to change state for
beforeTransition(event) => booleannoa function that run before the transition, if the result is truthy then the transition will terminate and the state will not change

Example:

waitBeforeBack: {
type: 'event',
event: 'animation-complete',
}

TimeoutTrigger

KeyTypeRequiredDescription
typeconstant: 'timeout'yesa constant to indicate the type of the trigger
timeoutnumberyesthe number of ms before the transition
forth: {
type: 'timeout',
timeout: 1000,
}

StateMachineDefiner

Beside from the StateMachineDefinition above, you can also use StateMachineDefiner which is a function to define your state machine like so:

const machineDef = ({world, owner}) => {
...
const back = ecs.defineState('back').initial()
.onEvent('position-animation-complete', 'waitBeforeForth')
.onEnter(() => {
...
})

const waitBeforeBack = ecs.defineState('waitBeforeBack').wait(1000, back)
const forth = ecs.defineState('forth')
.onEvent('position-animation-complete', waitBeforeBack)
.onEnter(() => {
...
})
ecs.defineState('waitBeforeForth').wait(1000, forth)
}

The StateMachineDefiner function accept a props with the following attributes:

AttributeTypeDescription
worldWorldthe world the machine is created in
ownerEidthe id of the owner of the machine

Inside the StateMachineDefiner you can define states using the function defineState below:

defineState()

メモ

defineState is a Fluent API, which mean you can chain all the subsequent modifier functions to define your state

defineState(name: string)

name: the unique name of the state in the state machine

defineState('foo')

.initial()

Mark this state as the initial state of the state machine

initial()

defineState(name).initial()

.wait()

wait(waitTime: number, nextState: string|State)

waitTime: the number of ms before this state transition to the next.

nextState: the next state that this timeout will transition into. Can be either a string that’s the name of the next state or another State object.

defineState(name).wait(waitTime, nextState)

.onEvent()

Trigger a transition to the next state when an event is received

onEvent(event: string, nextState: string|State, args?: {})

event: string - the event to listen to

nextState: string or State - the next state to transition to. You can pass another state returned by defineState or just the state name

args: object - optional arguments for the event. It has the following attributes

AttributeTypeRequiredDescription
targetEidnothe entity you want this trigger to change state for
beforeTransition(event) => booleanno a function that run before the transition, if the result is truthy then the transition will terminate and the state will not change
defineState('state').onEvent('event-name', 'next-state')

.onEnter()

Set a callback to run when entering this state.

callback: function - A callback function to run before state enter.

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

.onExit()

Set a callback to run when exiting this state.

callback: function - a callback function to run before state exiting.

defineState('state').onExit(() => {
// Do thing
})