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
Key | Type | Required | Description |
---|---|---|---|
initialState | string | yes | Name of the starting state of the state machine. |
states | Record<string, State> | yes | A 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
Key | Type | Required | Description |
---|---|---|---|
triggers | Record<string, Trigger[]> | yes | Name of the next states that this state and transition to and the triggers to do so. |
onEnter | function | no | a function called before the state is entered. |
onExit | function | no | a 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
Key | Type | Required | Description |
---|---|---|---|
type | constant: 'event' | yes a constant to indicate the type of the trigger | |
event | string | yes | the event type that trigger this |
target | Eid | no | the entity you want this trigger to change state for |
beforeTransition | (event) => boolean | no | a 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
Key | Type | Required | Description |
---|---|---|---|
type | constant: 'timeout' | yes | a constant to indicate the type of the trigger |
timeout | number | yes | the 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:
Attribute | Type | Description |
---|---|---|
world | World | the world the machine is created in |
owner | Eid | the 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
Attribute | Type | Required | Description |
---|---|---|---|
target | Eid | no | the entity you want this trigger to change state for |
beforeTransition | (event) => boolean | no | 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
})