Passer au contenu principal

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)
},
})