Events
Events are one of the basic tools for creating interactive and dynamic experiences. Events are how entities can communicate with each other through a flexible listener and dispatch system.
Event API
addListener
addListener registers a component to listen to an event.
Parameters
target: Eid
The eid of the entity that the listener is attached to.
name: string
The name of the event that the entity is listening for.
listener: Listener
The function that gets triggered when the event name is dispatched to the target entity or a entity that is a child of the entity the listener is attached to.
Listener Type:
type Event = {
target: Eid // The entity the event was dispatched on
currentTarget: Eid // The entity the event was listened on
name: string // The name of the event
data: unknown // The custom data emitted by the dispatcher
}
type Listener = (event: Event) => void
Example:
const handleCollision = (e) => { console.log("I touched another entity") }
// This listener logs a message whenever the entity collides with another entity.
world.events.addListener(component.eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
removeListener
removeListener un-registers an existing listener. The eid, name, and exact reference to the listener callback is needed to remove the listener.
Parameters
target: Eid
The eid of the entity that the listener is attached to.
name: string
The name of the event that the entity is listening for.
listener: Listener
The function that gets triggered when the event name is dispatched to the target entity or a entity that is a child of the entity the listener is attached to.
Example:
const handleCollision = (e) => { console.log("I touched another entity") }
// This listener logs a message whenever the entity collides with another entity.
world.events.addListener(component.eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
// This line removes the listener
world.events.removeListener(component.eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
dispatch
dispatch is used to emit events that the listeners can listen to.
Parameters
target: Eid
The entity that is you want the dispatch to target.
name: string
The name of the event you want to dispatch.
data: unknown
Custom data that is optionally emitted by the dispatch call.
Example:
// This line emits an event to the eid of an enemy entity with the name attack with the
// damage the attack caused stored as the data parameter.
world.events.dispatch(eidOfEnemy, "attack", {damage: 10})
Note that events bubble up the hierarchy tree. For a tree hierarchy consisting of:
- parent
- child
If an event is emitted on child
(child eid is passed as a target in the dispatch function), a listener on the parent will also receive the event (event.target
will be child, and event.currentTarget
will be parent).
globalId
Events can also be listened to regardless of target using globalId
as the target. This is how a listener can listen to every dispatched event regardless of where it is in the hierarchy.
Example:
world.events.addListener(world.events.globalId, eventName,(e) => {
console.log("Received", eventName, "on", e.target, "event:", e
})
Cleaning Up Listeners
Listeners are not cleaned up automatically when a component gets deleted so it has to be done manually. Here is a script containing helper function that will make the process easier. Add
cleanup.ts
to your studio app to use it.
cleanup.ts
import * as ecs from '@8thwall/ecs'
// eslint-disable-next-line arrow-parens
const createInstanced = <K extends object, V>(create: (k: K) => V) => {
const instanceMap = new WeakMap<K, V>()
return (key: K) => {
if (instanceMap.has(key)) {
return instanceMap.get(key)!
}
const instance = create(key)
instanceMap.set(key, instance)
return instance
}
}
type Cleanup = () => void
const cleanups = createInstanced<object, Map<ecs.Eid, Cleanup>>(() => new Map())
type ComponentCursor = {
eid: ecs.Eid
schemaAttribute: object
}
const addCleanup = (component: ComponentCursor, cleanup: Cleanup) => (
cleanups(component.schemaAttribute).set(component.eid, cleanup)
)
const doCleanup = (component: ComponentCursor) => {
// eslint-disable-next-line no-unused-expressions
cleanups(component.schemaAttribute).get(component.eid)?.()
cleanups(component.schemaAttribute).delete(component.eid)
}
export {
addCleanup,
doCleanup,
}
Example Usage:
import * as ecs from '@8thwall/ecs'
import {addCleanup, doCleanup} from '.cleanup'
ecs.registerComponent({
name: 'collision-logger',
add: (world, component) => {
// Runs when the component is added to the world.
const handleCollision = (e) => { console.log("I touched another entity") }
// This listener logs a message whenever the entity collides with another entity.
world.events.addListener(component.eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
const cleanup = () => {
world.events.removeListener(component.eid, 'damaged', handleCollision)
}
// This adds the cleanup function to a map that we can call when we want to cleanup the
// listener.
addCleanup(component, cleanup)
},
remove: (world, component) => {
// Runs when the component is removed from the world.
doCleanup(component)
},
})
Creating Handlers
Lets say we are creating a handler for a npc entity that listens for a damaged
event. The handler for the event changes some schema
and data
values. When creating handlers it might be tempting to do something like this.
import * as ecs from '@8thwall/ecs'
import {addCleanup, doCleanup} from '.cleanup'
ecs.registerComponent({
name: 'npc',
schema: {
// Add data that can be configured on the component.
isInjured: ecs.boolean,
},
data: {
// Add data that cannot be configured outside of the component.
bpm: ecs.f32,
},
add: (world, component) => {
// Runs when the component is added to the world.
const damagedHandler = (e) => {
component.schema.isInjured = true
component.data.bpm += 30
}
world.events.addListener(component.eid, 'damaged', damagedHandler)
const cleanup = () => {
world.events.removeListener(component.eid, 'damaged', damagedHandler)
}
// This adds the cleanup function to a map that we can call when we want to cleanup the
// listener.
addCleanup(component, cleanup)
},
remove: (world, component) => {
// Runs when the component is removed from the world.
doCleanup(component)
},
})
Unfortunately this is incorrect as component
can not be accessed in a callback like this. The handler will stale when this component is added to multiple entities.
The correct way to create the handler here is to pass in the component's dataAttributes
and schemaAttributes
and use that to fetch the cursor inside the handler.
import * as ecs from '@8thwall/ecs'
import {addCleanup, doCleanup} from '.cleanup'
const damagedHandler = (dataAttribute, schemaAttribute) => (e) => {
const dataCursor = dataAttribute.cursor(e.target)
const schemaCursor = schemaAttribute.cursor(e.target)
dataCursor.bpm += 30
schemaCursor.isInjured = true
}
ecs.registerComponent({
name: 'npc',
schema: {
// Add data that can be configured on the component.
isInjured: ecs.boolean,
},
data: {
// Add data that cannot be configured outside of the component.
bpm: ecs.f32,
},
add: (world, component) => {
// Runs when the component is added to the world.
const handler = damagedHandler(component.dataAttribute, component.schemaAttribute)
world.events.addListener(component.eid, 'damaged', handler)
const cleanup = () => {
world.events.removeListener(component.eid, 'damaged', handler)
}
// This adds the cleanup function to a map that we can call when we want to cleanup the
// listener.
addCleanup(component, cleanup)
},
remove: (world, component) => {
// Runs when the component is removed from the world.
doCleanup(component)
},
})