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
Property | Type | Description |
---|---|---|
world | World | Reference to the World. |
eid | eid | The Entity ID of the current Component |
schemaAttribute | WorldAttribute | Reference to the current Component's schema in World Scope. |
dataAttribute | WorldAttribute | Reference 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
Property | Type | Description |
---|---|---|
initialState (Required) | string | Name of the starting state of the state machine |
states (Required) | Record<string, State> | A map that stores state names and their definition |
groups | StateGroup[] | 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
Property | Type | Description |
---|---|---|
triggers (Required) | Record<string, Trigger[]> | Outgoing transitions, indexed by their target state |
onEnter | () => void | Function called upon entering the state |
onTick | () => void | Function called every frame while in the state |
onExit | () => void | Function called when exiting the state |
listeners | ListenerParams[] | 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)
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
Parameter | Type | Description |
---|---|---|
event (Required) | string | The name of the event to listen for |
nextState (Required) | stateId | The state to transition to when the event occurs |
args | object | Arguments used to determine transition conditions |
Args
Parameter | Type | Description |
---|---|---|
target | eid | The entity expected to receive the event (defaults to the state machine owner) |
where | (QueuedEvent) => boolean | An 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.
Parameter | Type | Description |
---|---|---|
timeout | number | The duration in milliseconds before transitioning |
nextState | StateId | The 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.
Parameter | Type | Description |
---|---|---|
handle | TriggerHandle | The handle that will cause a transition when manually activated |
nextState | StateId | The 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.
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 |
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.
Parameter | Type | Description |
---|---|---|
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'])
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
Parameter | Type | Description |
---|---|---|
event (Required) | string | The name of the event to listen for |
nextState (Required) | stateId | The state to transition to when the event occurs |
args | object | Arguments used to determine transition conditions |
Args
Parameter | Type | Description |
---|---|---|
target | eid | The entity expected to receive the event (defaults to the state machine owner) |
where | (QueuedEvent) => boolean | An 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.
Parameter | Type | Description |
---|---|---|
timeout | number | The duration in milliseconds before transitioning |
nextState | StateId | The 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.
Parameter | Type | Description |
---|---|---|
handle | TriggerHandle | The handle that will cause a transition when manually activated |
nextState | StateId | The 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.
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 |
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.
Parameter | Type | Description |
---|---|---|
trigger() | () => void | Call 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.
Parameter | Type | Description |
---|---|---|
type (Required) | 'event' | A constant to indicate the type of the trigger |
event (Required) | string | The name of the event to listen for |
target | eid | The entity that is expected to receive an event |
where | (QueuedEvent) => boolean | An 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.
Parameter | Type | Description |
---|---|---|
type (Required) | 'timeout' | A constant to indicate the type of the trigger |
timeout (Required) | number | The 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.
Parameter | Type | Description |
---|---|---|
type (Required) | 'custom' | A constant to indicate the type of the trigger |
timeout (Required) | number | The number of milliseconds to wait before transitioning |
const toOther = ecs.defineTrigger()
const example = {
triggers:
'other': [
{
type: 'custom',
trigger: toOther,
},
]
}
...
toOther.trigger()