Events
Nearly everything in Excalibur has a way to listen to events! This is useful when you want to know exactly when things happen in Excalibur and respond to them with game logic. Actors, Scenes, Engines, Actions, Animations, and various components have events you can hook into just look for the .events
member!
Excalibur events are handled synchronously, which is great for debugging and reducing timing bugs.
Strongly-typed Events
Excalibur has types on all it's event listeners, you can check these types with Intellisense in VS Code or by following the Typescript definition.
Pub/Sub or Signal-based Event Bus
Excalibur also allows you to listen/send any event you want to as well, but you'll need to provide your own types for that. At its core, the EventEmitter is a pub/sub mechanism (also called "Signals" in other engines), which means you can create an emitter as a way to pass messages between entities, components, or systems. This is how much of Excalibur works internally.
Example: Health Depletion
Here is an example of emitting a custom healthdepleted
event that is strongly-typed:
ts
typePlayerEvents = {healthdepleted :PlayerHealthDepletedEvent ;}export classPlayerHealthDepletedEvent extendsex .GameEvent <Player > {constructor(publictarget :Player ) {super();}}export constPlayerEvents = {Healthdepleted : 'healthdepleted'} asconst ;export classPlayer extendsex .Actor {publicevents = newex .EventEmitter <ex .ActorEvents &PlayerEvents >();privatehealth : number = 100;publiconPostUpdate () {if (this.health <= 0) {this.events .emit (PlayerEvents .Healthdepleted , newPlayerHealthDepletedEvent (this));}}}
ts
typePlayerEvents = {healthdepleted :PlayerHealthDepletedEvent ;}export classPlayerHealthDepletedEvent extendsex .GameEvent <Player > {constructor(publictarget :Player ) {super();}}export constPlayerEvents = {Healthdepleted : 'healthdepleted'} asconst ;export classPlayer extendsex .Actor {publicevents = newex .EventEmitter <ex .ActorEvents &PlayerEvents >();privatehealth : number = 100;publiconPostUpdate () {if (this.health <= 0) {this.events .emit (PlayerEvents .Healthdepleted , newPlayerHealthDepletedEvent (this));}}}
There are three main parts to this example:
- The
type
declaration holds the mapping of event name to event class. - A
class
representing the custom event which can extend GameEvent - A
const
declaration to provide a public way to pass the event name without using strings (optional)
The third is optional but recommended to avoid "magic strings" especially for events used all over your codebase.
Finally, you can expose an EventEmitter on your entity for other entities to subscribe/publish to. In this case, Actor.events is already provided by Excalibur, so you need to intersect your custom events with the existing events.
You don't have to override the existing Actor.events property. You could also export a static emitter, or use a different property name. This example emitter lifecycle is tied to the Player
and maintains a consistent way to emit other events but its just to illustrate a way to accomplish pub/sub.
Example: Strict Event Names
By default, EventEmitter is flexible to allow any string
event name and any event.
If you want more strictness with TypeScript, you can do it by deriving a custom event emitter that overrides the various overloaded methods to only allow strict event key names.
In this example, there's a typo, and a compiler error is thrown:
ts
typeStrictEventKey <TEventMap > = keyofTEventMap ;classStrictEventEmitter <TEventMap extendsex .EventMap > extendsex .EventEmitter <TEventMap > {emit <TEventName extendsStrictEventKey <TEventMap >>(eventName :TEventName ,event :TEventMap [TEventName ]): void;emit <TEventName extendsStrictEventKey <TEventMap > | string>(eventName :TEventName ,event ?:TEventMap [TEventName ]): void {super.emit (eventName as any,event as any);}}export classPlayer extendsex .Actor {publicevents = newStrictEventEmitter <ex .ActorEvents &PlayerEvents >();privatehealth : number = 100;publiconPostUpdate () {if (this.health <= 0) {this.Argument of type '"healthdpleted"' is not assignable to parameter of type '"healthdepleted" | keyof EntityEvents | "collisionstart" | "collisionend" | "precollision" | "postcollision" | "prekill" | "postkill" | "predraw" | "postdraw" | ... 19 more ... | "actioncomplete"'.2345Argument of type '"healthdpleted"' is not assignable to parameter of type '"healthdepleted" | keyof EntityEvents | "collisionstart" | "collisionend" | "precollision" | "postcollision" | "prekill" | "postkill" | "predraw" | "postdraw" | ... 19 more ... | "actioncomplete"'.events .emit ("healthdpleted" , newPlayerHealthDepletedEvent (this));}}}
ts
typeStrictEventKey <TEventMap > = keyofTEventMap ;classStrictEventEmitter <TEventMap extendsex .EventMap > extendsex .EventEmitter <TEventMap > {emit <TEventName extendsStrictEventKey <TEventMap >>(eventName :TEventName ,event :TEventMap [TEventName ]): void;emit <TEventName extendsStrictEventKey <TEventMap > | string>(eventName :TEventName ,event ?:TEventMap [TEventName ]): void {super.emit (eventName as any,event as any);}}export classPlayer extendsex .Actor {publicevents = newStrictEventEmitter <ex .ActorEvents &PlayerEvents >();privatehealth : number = 100;publiconPostUpdate () {if (this.health <= 0) {this.Argument of type '"healthdpleted"' is not assignable to parameter of type '"healthdepleted" | keyof EntityEvents | "collisionstart" | "collisionend" | "precollision" | "postcollision" | "prekill" | "postkill" | "predraw" | "postdraw" | ... 19 more ... | "actioncomplete"'.2345Argument of type '"healthdpleted"' is not assignable to parameter of type '"healthdepleted" | keyof EntityEvents | "collisionstart" | "collisionend" | "precollision" | "postcollision" | "prekill" | "postkill" | "predraw" | "postdraw" | ... 19 more ... | "actioncomplete"'.events .emit ("healthdpleted" , newPlayerHealthDepletedEvent (this));}}}
But when passing the appropriate constant, the error is gone:
ts
export classPlayer extendsex .Actor {publicevents = newStrictEventEmitter <ex .ActorEvents &PlayerEvents >();privatehealth : number = 100;publiconPostUpdate () {if (this.health <= 0) {this.events .emit (PlayerEvents .Healthdepleted , newPlayerHealthDepletedEvent (this));}}}
ts
export classPlayer extendsex .Actor {publicevents = newStrictEventEmitter <ex .ActorEvents &PlayerEvents >();privatehealth : number = 100;publiconPostUpdate () {if (this.health <= 0) {this.events .emit (PlayerEvents .Healthdepleted , newPlayerHealthDepletedEvent (this));}}}
This example is intentionally only showing the emit
method, but you can override all the methods in the same way.