From 032ff764c05231bc5bdaa9d86135940bcce0722c Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Mon, 24 Jul 2023 09:13:14 -0500 Subject: [PATCH] refactor: [#1145] Strong types EventEmitter refactor (#2702) Closes #1145 ## Changes: - Refactors events usage to the new EventEmitter type - Strongly typed events for each built in event --- CHANGELOG.md | 4 +- src/engine/Actor.ts | 302 ++++++------------ src/engine/Camera.ts | 62 ++-- src/engine/Class.ts | 58 ---- src/engine/Collision/BodyComponent.ts | 4 +- src/engine/Collision/ColliderComponent.ts | 8 +- src/engine/Collision/Colliders/Collider.ts | 4 +- .../Collision/Colliders/CompositeCollider.ts | 4 +- src/engine/Engine.ts | 117 +++---- src/engine/EntityComponentSystem/Entity.ts | 62 +++- src/engine/EventEmitter.ts | 44 +-- src/engine/Events.ts | 2 +- src/engine/Graphics/Animation.ts | 6 + src/engine/Graphics/OffscreenSystem.ts | 4 +- src/engine/Input/Gamepad.ts | 94 ++++-- src/engine/Input/Keyboard.ts | 60 +++- src/engine/Input/PointerAbstraction.ts | 46 +-- src/engine/Input/PointerEventReceiver.ts | 60 ++-- src/engine/Interfaces/Evented.ts | 10 +- src/engine/Loader.ts | 40 ++- src/engine/Resources/Resource.ts | 20 +- src/engine/Resources/Sound/Sound.ts | 67 +++- src/engine/Scene.ts | 102 +++--- src/engine/TileMap/TileMap.ts | 58 +++- src/engine/Trigger.ts | 18 +- src/engine/index.ts | 1 - src/spec/ColliderComponentSpec.ts | 4 +- src/spec/OffscreenSystemSpec.ts | 4 +- 28 files changed, 701 insertions(+), 564 deletions(-) delete mode 100644 src/engine/Class.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9acf284e..7fd0d3cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes -- +- Removed `ex.Class` base class type, this was a common base class for many excalibur types that provided old on/off event functionality. This functionality has been preserved on the types that had it before using `ex.EventEmitter` ### Deprecated @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added strongly typed events with `ex.EventEmitter` - Added new convenience properties for flipping all the graphics on an Actor * `ex.Actor.graphics.flipHorizontal` - Flips all the graphics horizontally * `ex.Actor.graphics.flipVertical` - Flips all the graphics vertically @@ -176,6 +177,7 @@ stored `ex.Graphics` causing them to be shared across clones. ### Changed +- Excalibur will now use `ex.EventEmitter` to broadcast events, Excalibur types that have events support will also have an `.events` member. - Excalibur resources by default no longer add cache busting query string to resources. All built in resources now expose a `bustCache` property to allow setting this before loading, for example `ex.Sound.bustCache`. diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 1f474ebe..c2926fc6 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -1,5 +1,4 @@ import { - InitializeEvent, KillEvent, PreUpdateEvent, PostUpdateEvent, @@ -9,11 +8,12 @@ import { CollisionEndEvent, PostKillEvent, PreKillEvent, - GameEvent, - ExitTriggerEvent, - EnterTriggerEvent, EnterViewPortEvent, - ExitViewPortEvent + ExitViewPortEvent, + PreDrawEvent, + PostDrawEvent, + PreDebugDrawEvent, + PostDebugDrawEvent } from './Events'; import { Engine } from './Engine'; import { Color } from './Color'; @@ -23,11 +23,10 @@ import { Logger } from './Util/Log'; import { Vector, vec } from './Math/vector'; import { BodyComponent } from './Collision/BodyComponent'; import { Eventable } from './Interfaces/Evented'; -import * as Events from './Events'; import { PointerEvents } from './Interfaces/PointerEventHandlers'; import { CollisionType } from './Collision/CollisionType'; -import { Entity } from './EntityComponentSystem/Entity'; +import { Entity, EntityEvents } from './EntityComponentSystem/Entity'; import { TransformComponent } from './EntityComponentSystem/Components/TransformComponent'; import { MotionComponent } from './EntityComponentSystem/Components/MotionComponent'; import { GraphicsComponent } from './Graphics/GraphicsComponent'; @@ -44,6 +43,7 @@ import { ActionsComponent } from './Actions/ActionsComponent'; import { Raster } from './Graphics/Raster'; import { Text } from './Graphics/Text'; import { CoordPlane } from './Math/coord-plane'; +import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; /** * Type guard for checking if something is an Actor @@ -140,6 +140,62 @@ export interface ActorArgs { collisionGroup?: CollisionGroup; } +export type ActorEvents = EntityEvents & { + collisionstart: CollisionStartEvent; + collisionend: CollisionEndEvent; + precollision: PreCollisionEvent; + postcollision: PostCollisionEvent; + kill: KillEvent; + prekill: PreKillEvent; + postkill: PostKillEvent; + predraw: PreDrawEvent; + postdraw: PostDrawEvent; + predebugdraw: PreDebugDrawEvent; + postdebugdraw: PostDebugDrawEvent; + pointerup: PointerEvent; + pointerdown: PointerEvent; + pointerenter: PointerEvent; + pointerleave: PointerEvent; + pointermove: PointerEvent; + pointercancel: PointerEvent; + pointerwheel: WheelEvent; + pointerdragstart: PointerEvent; + pointerdragend: PointerEvent; + pointerdragenter: PointerEvent; + pointerdragleave: PointerEvent; + pointerdragmove: PointerEvent; + enterviewport: EnterViewPortEvent; + exitviewport: ExitViewPortEvent; +} + +export const ActorEvents = { + CollisionStart: 'collisionstart', + CollisionEnd: 'collisionend', + PreCollision: 'precollision', + PostCollision: 'postcollision', + Kill: 'kill', + PreKill: 'prekill', + PostKill: 'postkill', + PreDraw: 'predraw', + PostDraw: 'postdraw', + PreDebugDraw: 'predebugdraw', + PostDebugDraw: 'postdebugdraw', + PointerUp: 'pointerup', + PointerDown: 'pointerdown', + PointerEnter: 'pointerenter', + PointerLeave: 'pointerleave', + PointerMove: 'pointermove', + PointerCancel: 'pointercancel', + Wheel: 'pointerwheel', + PointerDrag: 'pointerdragstart', + PointerDragEnd: 'pointerdragend', + PointerDragEnter: 'pointerdragenter', + PointerDragLeave: 'pointerdragleave', + PointerDragMove: 'pointerdragmove', + EnterViewPort: 'enterviewport', + ExitViewPort: 'exitviewport' +}; + /** * The most important primitive in Excalibur is an `Actor`. Anything that * can move on the screen, collide with another `Actor`, respond to events, @@ -147,6 +203,7 @@ export interface ActorArgs { * be part of a [[Scene]] for it to be drawn to the screen. */ export class Actor extends Entity implements Eventable, PointerEvents, CanInitialize, CanUpdate, CanBeKilled { + public events = new EventEmitter(); // #region Properties /** @@ -402,15 +459,15 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia public set draggable(isDraggable: boolean) { if (isDraggable) { if (isDraggable && !this._draggable) { - this.on('pointerdragstart', this._pointerDragStartHandler); - this.on('pointerdragend', this._pointerDragEndHandler); - this.on('pointerdragmove', this._pointerDragMoveHandler); - this.on('pointerdragleave', this._pointerDragLeaveHandler); + this.events.on('pointerdragstart', this._pointerDragStartHandler); + this.events.on('pointerdragend', this._pointerDragEndHandler); + this.events.on('pointerdragmove', this._pointerDragMoveHandler); + this.events.on('pointerdragleave', this._pointerDragLeaveHandler); } else if (!isDraggable && this._draggable) { - this.off('pointerdragstart', this._pointerDragStartHandler); - this.off('pointerdragend', this._pointerDragEndHandler); - this.off('pointerdragmove', this._pointerDragMoveHandler); - this.off('pointerdragleave', this._pointerDragLeaveHandler); + this.events.off('pointerdragstart', this._pointerDragStartHandler); + this.events.off('pointerdragend', this._pointerDragEndHandler); + this.events.off('pointerdragmove', this._pointerDragMoveHandler); + this.events.off('pointerdragleave', this._pointerDragLeaveHandler); } this._draggable = isDraggable; @@ -571,190 +628,29 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia } // #region Events + public emit>(eventName: TEventName, event: ActorEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } - public on(eventName: Events.exittrigger, handler: (event: ExitTriggerEvent) => void): void; - public on(eventName: Events.entertrigger, handler: (event: EnterTriggerEvent) => void): void; - /** - * The **collisionstart** event is fired when a [[BodyComponent|physics body]], usually attached to an actor, - * first starts colliding with another [[BodyComponent|body]], and will not fire again while in contact until - * the the pair separates and collides again. - * Use cases for the **collisionstart** event may be detecting when an actor has touched a surface - * (like landing) or if a item has been touched and needs to be picked up. - */ - public on(eventName: Events.collisionstart, handler: (event: CollisionStartEvent) => void): void; - /** - * The **collisionend** event is fired when two [[BodyComponent|physics bodies]] are no longer in contact. - * This event will not fire again until another collision and separation. - * - * Use cases for the **collisionend** event might be to detect when an actor has left a surface - * (like jumping) or has left an area. - */ - public on(eventName: Events.collisionend, handler: (event: CollisionEndEvent) => void): void; - /** - * The **precollision** event is fired **every frame** where a collision pair is found and two - * bodies are intersecting. - * - * This event is useful for building in custom collision resolution logic in Passive-Passive or - * Active-Passive scenarios. For example in a breakout game you may want to tweak the angle of - * ricochet of the ball depending on which side of the paddle you hit. - */ - public on(eventName: Events.precollision, handler: (event: PreCollisionEvent) => void): void; - /** - * The **postcollision** event is fired for **every frame** where collision resolution was performed. - * Collision resolution is when two bodies influence each other and cause a response like bouncing - * off one another. It is only possible to have *postcollision* event in Active-Active and Active-Fixed - * type collision pairs. - * - * Post collision would be useful if you need to know that collision resolution is happening or need to - * tweak the default resolution. - */ - public on(eventName: Events.postcollision, handler: (event: PostCollisionEvent) => void): void; - public on(eventName: Events.kill, handler: (event: KillEvent) => void): void; - public on(eventName: Events.prekill, handler: (event: PreKillEvent) => void): void; - public on(eventName: Events.postkill, handler: (event: PostKillEvent) => void): void; - public on(eventName: Events.initialize, handler: (event: InitializeEvent) => void): void; - public on(eventName: Events.preupdate, handler: (event: PreUpdateEvent) => void): void; - public on(eventName: Events.postupdate, handler: (event: PostUpdateEvent) => void): void; - public on(eventName: Events.pointerup, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerdown, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerenter, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerleave, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointermove, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointercancel, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerwheel, handler: (event: WheelEvent) => void): void; - public on(eventName: Events.pointerdragstart, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerdragend, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerdragenter, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerdragleave, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.pointerdragmove, handler: (event: PointerEvent) => void): void; - public on(eventName: Events.enterviewport, handler: (event: EnterViewPortEvent) => void): void; - public on(eventName: Events.exitviewport, handler: (event: ExitViewPortEvent) => void): void; - public on(eventName: string, handler: (event: GameEvent) => void): void; - public on(eventName: string, handler: (event: any) => void): void { - super.on(eventName, handler); - } - - public once(eventName: Events.exittrigger, handler: (event: ExitTriggerEvent) => void): void; - public once(eventName: Events.entertrigger, handler: (event: EnterTriggerEvent) => void): void; - /** - * The **collisionstart** event is fired when a [[BodyComponent|physics body]], usually attached to an actor, - * first starts colliding with another [[BodyComponent|body]], and will not fire again while in contact until - * the the pair separates and collides again. - * Use cases for the **collisionstart** event may be detecting when an actor has touch a surface - * (like landing) or if a item has been touched and needs to be picked up. - */ - public once(eventName: Events.collisionstart, handler: (event: CollisionStartEvent) => void): void; - /** - * The **collisionend** event is fired when two [[BodyComponent|physics bodies]] are no longer in contact. - * This event will not fire again until another collision and separation. - * - * Use cases for the **collisionend** event might be to detect when an actor has left a surface - * (like jumping) or has left an area. - */ - public once(eventName: Events.collisionend, handler: (event: CollisionEndEvent) => void): void; - /** - * The **precollision** event is fired **every frame** where a collision pair is found and two - * bodies are intersecting. - * - * This event is useful for building in custom collision resolution logic in Passive-Passive or - * Active-Passive scenarios. For example in a breakout game you may want to tweak the angle of - * ricochet of the ball depending on which side of the paddle you hit. - */ - public once(eventName: Events.precollision, handler: (event: PreCollisionEvent) => void): void; - /** - * The **postcollision** event is fired for **every frame** where collision resolution was performed. - * Collision resolution is when two bodies influence each other and cause a response like bouncing - * off one another. It is only possible to have *postcollision* event in Active-Active and Active-Fixed - * type collision pairs. - * - * Post collision would be useful if you need to know that collision resolution is happening or need to - * tweak the default resolution. - */ - public once(eventName: Events.postcollision, handler: (event: PostCollisionEvent) => void): void; - public once(eventName: Events.kill, handler: (event: KillEvent) => void): void; - public once(eventName: Events.postkill, handler: (event: PostKillEvent) => void): void; - public once(eventName: Events.prekill, handler: (event: PreKillEvent) => void): void; - public once(eventName: Events.initialize, handler: (event: InitializeEvent) => void): void; - public once(eventName: Events.preupdate, handler: (event: PreUpdateEvent) => void): void; - public once(eventName: Events.postupdate, handler: (event: PostUpdateEvent) => void): void; - public once(eventName: Events.pointerup, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerdown, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerenter, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerleave, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointermove, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointercancel, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerwheel, handler: (event: WheelEvent) => void): void; - public once(eventName: Events.pointerdragstart, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerdragend, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerdragenter, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerdragleave, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.pointerdragmove, handler: (event: PointerEvent) => void): void; - public once(eventName: Events.enterviewport, handler: (event: EnterViewPortEvent) => void): void; - public once(eventName: Events.exitviewport, handler: (event: ExitViewPortEvent) => void): void; - public once(eventName: string, handler: (event: GameEvent) => void): void; - public once(eventName: string, handler: (event: any) => void): void { - super.once(eventName, handler); - } - - public off(eventName: Events.exittrigger, handler?: (event: ExitTriggerEvent) => void): void; - public off(eventName: Events.entertrigger, handler?: (event: EnterTriggerEvent) => void): void; - /** - * The **collisionstart** event is fired when a [[BodyComponent|physics body]], usually attached to an actor, - * first starts colliding with another [[BodyComponent|body]], and will not fire again while in contact until - * the the pair separates and collides again. - * Use cases for the **collisionstart** event may be detecting when an actor has touch a surface - * (like landing) or if a item has been touched and needs to be picked up. - */ - public off(eventName: Events.collisionstart, handler?: (event: CollisionStartEvent) => void): void; - /** - * The **collisionend** event is fired when two [[BodyComponent|physics bodies]] are no longer in contact. - * This event will not fire again until another collision and separation. - * - * Use cases for the **collisionend** event might be to detect when an actor has left a surface - * (like jumping) or has left an area. - */ - public off(eventName: Events.collisionend, handler?: (event: CollisionEndEvent) => void): void; - /** - * The **precollision** event is fired **every frame** where a collision pair is found and two - * bodies are intersecting. - * - * This event is useful for building in custom collision resolution logic in Passive-Passive or - * Active-Passive scenarios. For example in a breakout game you may want to tweak the angle of - * ricochet of the ball depending on which side of the paddle you hit. - */ - public off(eventName: Events.precollision, handler?: (event: PreCollisionEvent) => void): void; - /** - * The **postcollision** event is fired for **every frame** where collision resolution was performed. - * Collision resolution is when two bodies influence each other and cause a response like bouncing - * off one another. It is only possible to have *postcollision* event in Active-Active and Active-Fixed - * type collision pairs. - * - * Post collision would be useful if you need to know that collision resolution is happening or need to - * tweak the default resolution. - */ - public off(eventName: Events.postcollision, handler: (event: PostCollisionEvent) => void): void; - public off(eventName: Events.pointerup, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerdown, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerenter, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerleave, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointermove, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointercancel, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerwheel, handler?: (event: WheelEvent) => void): void; - public off(eventName: Events.pointerdragstart, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerdragend, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerdragenter, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerdragleave, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.pointerdragmove, handler?: (event: PointerEvent) => void): void; - public off(eventName: Events.prekill, handler?: (event: PreKillEvent) => void): void; - public off(eventName: Events.postkill, handler?: (event: PostKillEvent) => void): void; - public off(eventName: Events.initialize, handler?: (event: Events.InitializeEvent) => void): void; - public off(eventName: Events.postupdate, handler?: (event: Events.PostUpdateEvent) => void): void; - public off(eventName: Events.preupdate, handler?: (event: Events.PreUpdateEvent) => void): void; - public off(eventName: Events.enterviewport, handler?: (event: EnterViewPortEvent) => void): void; - public off(eventName: Events.exitviewport, handler?: (event: ExitViewPortEvent) => void): void; - public off(eventName: string, handler?: (event: GameEvent) => void): void; - public off(eventName: string, handler?: (event: any) => void): void { - super.off(eventName, handler); + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); + } + + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } // #endregion @@ -766,7 +662,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia * @internal */ public _prekill(_scene: Scene) { - super.emit('prekill', new PreKillEvent(this)); + this.events.emit('prekill', new PreKillEvent(this)); this.onPreKill(_scene); } @@ -786,7 +682,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia * @internal */ public _postkill(_scene: Scene) { - super.emit('postkill', new PostKillEvent(this)); + this.events.emit('postkill', new PostKillEvent(this)); this.onPostKill(_scene); } @@ -806,7 +702,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia public kill() { if (this.scene) { this._prekill(this.scene); - this.emit('kill', new KillEvent(this)); + this.events.emit('kill', new KillEvent(this)); super.kill(); this._postkill(this.scene); } else { @@ -986,7 +882,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia * @internal */ public _preupdate(engine: Engine, delta: number): void { - this.emit('preupdate', new PreUpdateEvent(engine, delta, this)); + this.events.emit('preupdate', new PreUpdateEvent(engine, delta, this)); this.onPreUpdate(engine, delta); } @@ -997,7 +893,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia * @internal */ public _postupdate(engine: Engine, delta: number): void { - this.emit('postupdate', new PostUpdateEvent(engine, delta, this)); + this.events.emit('postupdate', new PostUpdateEvent(engine, delta, this)); this.onPostUpdate(engine, delta); } diff --git a/src/engine/Camera.ts b/src/engine/Camera.ts index 606adbc8..4872d449 100644 --- a/src/engine/Camera.ts +++ b/src/engine/Camera.ts @@ -5,13 +5,13 @@ import { Vector, vec } from './Math/vector'; import { Actor } from './Actor'; import { removeItemFromArray } from './Util/Util'; import { CanUpdate, CanInitialize } from './Interfaces/LifecycleEvents'; -import { PreUpdateEvent, PostUpdateEvent, GameEvent, InitializeEvent } from './Events'; -import { Class } from './Class'; +import { PreUpdateEvent, PostUpdateEvent, InitializeEvent } from './Events'; import { BoundingBox } from './Collision/BoundingBox'; import { Logger } from './Util/Log'; import { ExcaliburGraphicsContext } from './Graphics/Context/ExcaliburGraphicsContext'; import { watchAny } from './Util/Watch'; import { AffineMatrix } from './Math/affine-matrix'; +import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; /** * Interface that describes a custom camera strategy for tracking targets @@ -230,6 +230,19 @@ export class LimitCameraBoundsStrategy implements CameraStrategy { }; } +export type CameraEvents = { + preupdate: PreUpdateEvent, + postupdate: PostUpdateEvent, + initialize: InitializeEvent, + +} + +export const CameraEvents = { + Initialize: 'initialize', + PreUpdate: 'preupdate', + PostUpdate: 'postupdate' +}; + /** * Cameras * @@ -238,7 +251,8 @@ export class LimitCameraBoundsStrategy implements CameraStrategy { * what is "off screen" and can be used to scale the game. * */ -export class Camera extends Class implements CanUpdate, CanInitialize { +export class Camera implements CanUpdate, CanInitialize { + public events = new EventEmitter(); public transform: AffineMatrix = AffineMatrix.identity(); public inverse: AffineMatrix = AffineMatrix.identity(); @@ -547,7 +561,7 @@ export class Camera extends Class implements CanUpdate, CanInitialize { * @internal */ public _preupdate(engine: Engine, delta: number): void { - this.emit('preupdate', new PreUpdateEvent(engine, delta, this)); + this.events.emit('preupdate', new PreUpdateEvent(engine, delta, this)); this.onPreUpdate(engine, delta); } @@ -567,7 +581,7 @@ export class Camera extends Class implements CanUpdate, CanInitialize { * @internal */ public _postupdate(engine: Engine, delta: number): void { - this.emit('postupdate', new PostUpdateEvent(engine, delta, this)); + this.events.emit('postupdate', new PostUpdateEvent(engine, delta, this)); this.onPostUpdate(engine, delta); } @@ -625,7 +639,7 @@ export class Camera extends Class implements CanUpdate, CanInitialize { this.updateTransform(); this.onInitialize(_engine); - super.emit('initialize', new InitializeEvent(_engine, this)); + this.events.emit('initialize', new InitializeEvent(_engine, this)); this._isInitialized = true; } } @@ -639,27 +653,29 @@ export class Camera extends Class implements CanUpdate, CanInitialize { // Overridable } - public on(eventName: 'initialize', handler: (event: InitializeEvent) => void): void; - public on(eventName: 'preupdate', handler: (event: PreUpdateEvent) => void): void; - public on(eventName: 'postupdate', handler: (event: PostUpdateEvent) => void): void; - public on(eventName: any, handler: any) { - super.on(eventName, handler); + public emit>(eventName: TEventName, event: CameraEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); } - public off(eventName: 'initialize', handler?: (event: InitializeEvent) => void): void; - public off(eventName: 'preupdate', handler?: (event: PreUpdateEvent) => void): void; - public off(eventName: 'postupdate', handler?: (event: PostUpdateEvent) => void): void; - public off(eventName: string, handler: (event: GameEvent) => void): void; - public off(eventName: string, handler: (event: any) => void): void { - super.off(eventName, handler); + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); } - public once(eventName: 'initialize', handler: (event: InitializeEvent) => void): void; - public once(eventName: 'preupdate', handler: (event: PreUpdateEvent) => void): void; - public once(eventName: 'postupdate', handler: (event: PostUpdateEvent) => void): void; - public once(eventName: string, handler: (event: GameEvent) => void): void; - public once(eventName: string, handler: (event: any) => void): void { - super.once(eventName, handler); + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } public runStrategies(engine: Engine, delta: number) { diff --git a/src/engine/Class.ts b/src/engine/Class.ts deleted file mode 100644 index 08056499..00000000 --- a/src/engine/Class.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { EventDispatcher } from './EventDispatcher'; -import { Eventable } from './Interfaces/Evented'; - -/** - * Excalibur base class that provides basic functionality such as [[EventDispatcher]] - * and extending abilities for vanilla Javascript projects - */ -export class Class implements Eventable { - /** - * Direct access to the game object event dispatcher. - */ - public eventDispatcher: EventDispatcher; - - constructor() { - this.eventDispatcher = new EventDispatcher(); - } - - /** - * Alias for `addEventListener`. You can listen for a variety of - * events off of the engine; see the events section below for a complete list. - * @param eventName Name of the event to listen for - * @param handler Event handler for the thrown event - */ - public on(eventName: string, handler: (event: any) => void) { - this.eventDispatcher.on(eventName, handler); - } - - /** - * Alias for `removeEventListener`. If only the eventName is specified - * it will remove all handlers registered for that specific event. If the eventName - * and the handler instance are specified only that handler will be removed. - * - * @param eventName Name of the event to listen for - * @param handler Event handler for the thrown event - */ - public off(eventName: string, handler?: (event: any) => void) { - this.eventDispatcher.off(eventName, handler); - } - - /** - * Emits a new event - * @param eventName Name of the event to emit - * @param eventObject Data associated with this event - */ - public emit(eventName: string, eventObject: any) { - this.eventDispatcher.emit(eventName, eventObject); - } - - /** - * Once listens to an event one time, then unsubscribes from that event - * - * @param eventName The name of the event to subscribe to once - * @param handler The handler of the event that will be auto unsubscribed - */ - public once(eventName: string, handler: (event: any) => void) { - this.eventDispatcher.once(eventName, handler); - } -} diff --git a/src/engine/Collision/BodyComponent.ts b/src/engine/Collision/BodyComponent.ts index c9b32d49..03f470a9 100644 --- a/src/engine/Collision/BodyComponent.ts +++ b/src/engine/Collision/BodyComponent.ts @@ -6,11 +6,11 @@ import { TransformComponent } from '../EntityComponentSystem/Components/Transfor import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; import { Component } from '../EntityComponentSystem/Component'; import { CollisionGroup } from './Group/CollisionGroup'; -import { EventDispatcher } from '../EventDispatcher'; import { createId, Id } from '../Id'; import { clamp } from '../Math/util'; import { ColliderComponent } from './ColliderComponent'; import { Transform } from '../Math/transform'; +import { EventEmitter } from '../EventEmitter'; export interface BodyComponentOptions { type?: CollisionType; @@ -33,7 +33,7 @@ export class BodyComponent extends Component<'ex.body'> implements Clonable = createId('body', BodyComponent._ID++); - public events = new EventDispatcher(); + public events = new EventEmitter(); private _oldTransform = new Transform(); diff --git a/src/engine/Collision/ColliderComponent.ts b/src/engine/Collision/ColliderComponent.ts index 6650930f..fb508b89 100644 --- a/src/engine/Collision/ColliderComponent.ts +++ b/src/engine/Collision/ColliderComponent.ts @@ -2,7 +2,6 @@ import { Vector } from '../Math/vector'; import { TransformComponent } from '../EntityComponentSystem'; import { Component } from '../EntityComponentSystem/Component'; import { Entity } from '../EntityComponentSystem/Entity'; -import { EventDispatcher } from '../EventDispatcher'; import { CollisionEndEvent, CollisionStartEvent, PostCollisionEvent, PreCollisionEvent } from '../Events'; import { Observable } from '../Util/Observable'; import { BoundingBox } from './BoundingBox'; @@ -13,11 +12,12 @@ import { CompositeCollider } from './Colliders/CompositeCollider'; import { PolygonCollider } from './Colliders/PolygonCollider'; import { EdgeCollider } from './Colliders/EdgeCollider'; import { Shape } from './Colliders/Shape'; +import { EventEmitter } from '../EventEmitter'; export class ColliderComponent extends Component<'ex.collider'> { public readonly type = 'ex.collider'; - public events = new EventDispatcher(); + public events = new EventEmitter(); /** * Observable that notifies when a collider is added to the body */ @@ -51,7 +51,7 @@ export class ColliderComponent extends Component<'ex.collider'> { if (collider) { this._collider = collider; this._collider.owner = this.owner; - this.events.wire(collider.events); + collider.events.pipe(this.events); this.$colliderAdded.notifyAll(collider); this.update(); } @@ -63,7 +63,7 @@ export class ColliderComponent extends Component<'ex.collider'> { */ public clear() { if (this._collider) { - this.events.unwire(this._collider.events); + this._collider.events.unpipe(this.events); this.$colliderRemoved.notifyAll(this._collider); this._collider.owner = null; this._collider = null; diff --git a/src/engine/Collision/Colliders/Collider.ts b/src/engine/Collision/Colliders/Collider.ts index 84c4ee6f..7b5c1e82 100644 --- a/src/engine/Collision/Colliders/Collider.ts +++ b/src/engine/Collision/Colliders/Collider.ts @@ -8,9 +8,9 @@ import { Ray } from '../../Math/ray'; import { Clonable } from '../../Interfaces/Clonable'; import { Entity } from '../../EntityComponentSystem'; import { createId, Id } from '../../Id'; -import { EventDispatcher } from '../../EventDispatcher'; import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext'; import { Transform } from '../../Math/transform'; +import { EventEmitter } from '../../EventEmitter'; /** * A collision collider specifies the geometry that can detect when other collision colliders intersect @@ -25,7 +25,7 @@ export abstract class Collider implements Clonable { * @hidden */ public __compositeColliderId: Id<'collider'> | null = null; - public events: EventDispatcher = new EventDispatcher(); + public events = new EventEmitter(); /** * Returns a boolean indicating whether this body collided with diff --git a/src/engine/Collision/Colliders/CompositeCollider.ts b/src/engine/Collision/Colliders/CompositeCollider.ts index f536091e..6ed7b7d6 100644 --- a/src/engine/Collision/Colliders/CompositeCollider.ts +++ b/src/engine/Collision/Colliders/CompositeCollider.ts @@ -31,7 +31,7 @@ export class CompositeCollider extends Collider { } addCollider(collider: Collider) { - this.events.wire(collider.events); + collider.events.pipe(this.events); collider.__compositeColliderId = this.id; this._colliders.push(collider); this._collisionProcessor.track(collider); @@ -39,7 +39,7 @@ export class CompositeCollider extends Collider { } removeCollider(collider: Collider) { - this.events.unwire(collider.events); + collider.events.pipe(this.events); collider.__compositeColliderId = null; Util.removeItemFromArray(collider, this._colliders); this._collisionProcessor.untrack(collider); diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index ffac2910..95c2371c 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -1,4 +1,5 @@ import { EX_VERSION } from './'; +import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; import { Gamepads } from './Input/Gamepad'; import { Keyboard } from './Input/Keyboard'; import { PointerScope } from './Input/PointerScope'; @@ -25,7 +26,6 @@ import { PostUpdateEvent, PreFrameEvent, PostFrameEvent, - GameEvent, DeactivateEvent, ActivateEvent, PreDrawEvent, @@ -37,8 +37,6 @@ import { Color } from './Color'; import { Scene } from './Scene'; import { Entity } from './EntityComponentSystem/Entity'; import { Debug, DebugStats } from './Debug/Debug'; -import { Class } from './Class'; -import * as Events from './Events'; import { BrowserEvents } from './Util/Browser'; import { ExcaliburGraphicsContext, ExcaliburGraphicsContext2DCanvas, ExcaliburGraphicsContextWebGL, TextureLoader } from './Graphics'; import { PointerEventReceiver } from './Input/PointerEventReceiver'; @@ -47,6 +45,36 @@ import { ImageFiltering } from './Graphics/Filtering'; import { GraphicsDiagnostics } from './Graphics/GraphicsDiagnostics'; import { Toaster } from './Util/Toaster'; +export type EngineEvents = { + fallbackgraphicscontext: ExcaliburGraphicsContext2DCanvas, + initialize: InitializeEvent, + visible: VisibleEvent, + hidden: HiddenEvent, + start: GameStartEvent, + stop: GameStopEvent, + preupdate: PreUpdateEvent, + postupdate: PostUpdateEvent, + preframe: PreFrameEvent, + postframe: PostFrameEvent, + predraw: PreDrawEvent, + postdraw: PostDrawEvent, +} + +export const EngineEvents = { + FallbackGraphicsContext: 'fallbackgraphicscontext', + Initialize: 'initialize', + Visible: 'visible', + Hidden: 'hidden', + Start: 'start', + Stop: 'stop', + PreUpdate: 'preupdate', + PostUpdate: 'postupdate', + PreFrame: 'preframe', + PostFrame: 'postframe', + PreDraw: 'predraw', + PostDraw: 'postdraw' +} as const; + /** * Enum representing the different mousewheel event bubble prevention */ @@ -249,7 +277,12 @@ export interface EngineOptions { * starting/stopping the game, maintaining state, transmitting events, * loading resources, and managing the scene. */ -export class Engine extends Class implements CanInitialize, CanUpdate, CanDraw { +export class Engine implements CanInitialize, CanUpdate, CanDraw { + /** + * Listen to and emit events on the Engine + */ + public events = new EventEmitter(); + /** * Excalibur browser events abstraction used for wiring to native browser events safely */ @@ -487,55 +520,29 @@ export class Engine extends Class implements CanInitialize, CanUpdate, CanDraw { private _deferredGoTo: string = null; - public on(eventName: 'fallbackgraphicscontext', handler: (event: ExcaliburGraphicsContext2DCanvas) => void): void; - public on(eventName: Events.initialize, handler: (event: Events.InitializeEvent) => void): void; - public on(eventName: Events.visible, handler: (event: VisibleEvent) => void): void; - public on(eventName: Events.hidden, handler: (event: HiddenEvent) => void): void; - public on(eventName: Events.start, handler: (event: GameStartEvent) => void): void; - public on(eventName: Events.stop, handler: (event: GameStopEvent) => void): void; - public on(eventName: Events.preupdate, handler: (event: PreUpdateEvent) => void): void; - public on(eventName: Events.postupdate, handler: (event: PostUpdateEvent) => void): void; - public on(eventName: Events.preframe, handler: (event: PreFrameEvent) => void): void; - public on(eventName: Events.postframe, handler: (event: PostFrameEvent) => void): void; - public on(eventName: Events.predraw, handler: (event: PreDrawEvent) => void): void; - public on(eventName: Events.postdraw, handler: (event: PostDrawEvent) => void): void; - public on(eventName: string, handler: (event: GameEvent) => void): void; - public on(eventName: string, handler: (event: any) => void): void { - super.on(eventName, handler); + public emit>(eventName: TEventName, event: EngineEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); } - public once(eventName: 'fallbackgraphicscontext', handler: (event: ExcaliburGraphicsContext2DCanvas) => void): void; - public once(eventName: Events.initialize, handler: (event: Events.InitializeEvent) => void): void; - public once(eventName: Events.visible, handler: (event: VisibleEvent) => void): void; - public once(eventName: Events.hidden, handler: (event: HiddenEvent) => void): void; - public once(eventName: Events.start, handler: (event: GameStartEvent) => void): void; - public once(eventName: Events.stop, handler: (event: GameStopEvent) => void): void; - public once(eventName: Events.preupdate, handler: (event: PreUpdateEvent) => void): void; - public once(eventName: Events.postupdate, handler: (event: PostUpdateEvent) => void): void; - public once(eventName: Events.preframe, handler: (event: PreFrameEvent) => void): void; - public once(eventName: Events.postframe, handler: (event: PostFrameEvent) => void): void; - public once(eventName: Events.predraw, handler: (event: PreDrawEvent) => void): void; - public once(eventName: Events.postdraw, handler: (event: PostDrawEvent) => void): void; - public once(eventName: string, handler: (event: GameEvent) => void): void; - public once(eventName: string, handler: (event: any) => void): void { - super.once(eventName, handler); + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); } - public off(eventName: 'fallbackgraphicscontext', handler?: (event: ExcaliburGraphicsContext2DCanvas) => void): void; - public off(eventName: Events.initialize, handler?: (event: Events.InitializeEvent) => void): void; - public off(eventName: Events.visible, handler?: (event: VisibleEvent) => void): void; - public off(eventName: Events.hidden, handler?: (event: HiddenEvent) => void): void; - public off(eventName: Events.start, handler?: (event: GameStartEvent) => void): void; - public off(eventName: Events.stop, handler?: (event: GameStopEvent) => void): void; - public off(eventName: Events.preupdate, handler?: (event: PreUpdateEvent) => void): void; - public off(eventName: Events.postupdate, handler?: (event: PostUpdateEvent) => void): void; - public off(eventName: Events.preframe, handler?: (event: PreFrameEvent) => void): void; - public off(eventName: Events.postframe, handler?: (event: PostFrameEvent) => void): void; - public off(eventName: Events.predraw, handler?: (event: PreDrawEvent) => void): void; - public off(eventName: Events.postdraw, handler?: (event: PostDrawEvent) => void): void; - public off(eventName: string, handler?: (event: GameEvent) => void): void; - public off(eventName: string, handler?: (event: any) => void): void { - super.off(eventName, handler); + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } /** @@ -592,8 +599,6 @@ export class Engine extends Class implements CanInitialize, CanUpdate, CanDraw { * ``` */ constructor(options?: EngineOptions) { - super(); - options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options }; this._originalOptions = options; @@ -1068,7 +1073,7 @@ O|===|* >________________>\n\ if (this.currentScene.isInitialized) { const context = { engine: this, previousScene, nextScene }; this.currentScene._deactivate.apply(this.currentScene, [context, nextScene]); - this.currentScene.eventDispatcher.emit('deactivate', new DeactivateEvent(context, this.currentScene)); + this.currentScene.events.emit('deactivate', new DeactivateEvent(context, this.currentScene)); } // set current scene to new one @@ -1080,7 +1085,7 @@ O|===|* >________________>\n\ const context = { engine: this, previousScene, nextScene, data }; this.currentScene._activate.apply(this.currentScene, [context, nextScene]); - this.currentScene.eventDispatcher.emit('activate', new ActivateEvent(context, this.currentScene)); + this.currentScene.events.emit('activate', new ActivateEvent(context, this.currentScene)); } else { this._logger.error('Scene', key, 'does not exist!'); } @@ -1128,10 +1133,10 @@ O|===|* >________________>\n\ this.browser.document.on('visibilitychange', () => { if (document.visibilityState === 'hidden') { - this.eventDispatcher.emit('hidden', new HiddenEvent(this)); + this.events.emit('hidden', new HiddenEvent(this)); this._logger.debug('Window hidden'); } else if (document.visibilityState === 'visible') { - this.eventDispatcher.emit('visible', new VisibleEvent(this)); + this.events.emit('visible', new VisibleEvent(this)); this._logger.debug('Window visible'); } }); @@ -1172,7 +1177,7 @@ O|===|* >________________>\n\ private _overrideInitialize(engine: Engine) { if (!this.isInitialized) { this.onInitialize(engine); - super.emit('initialize', new InitializeEvent(engine, this)); + this.events.emit('initialize', new InitializeEvent(engine, this)); this._isInitialized = true; if (this._deferredGoTo) { const deferredScene = this._deferredGoTo; diff --git a/src/engine/EntityComponentSystem/Entity.ts b/src/engine/EntityComponentSystem/Entity.ts index 88b0230b..ba07b202 100644 --- a/src/engine/EntityComponentSystem/Entity.ts +++ b/src/engine/EntityComponentSystem/Entity.ts @@ -1,12 +1,10 @@ import { Component, ComponentCtor, TagComponent } from './Component'; import { Observable, Message } from '../Util/Observable'; -import { Class } from '../Class'; import { OnInitialize, OnPreUpdate, OnPostUpdate } from '../Interfaces/LifecycleEvents'; import { Engine } from '../Engine'; import { InitializeEvent, PreUpdateEvent, PostUpdateEvent } from '../Events'; -import { EventDispatcher } from '../EventDispatcher'; -import { Scene, Util } from '..'; +import { EventEmitter, EventKey, Handler, Scene, Subscription, Util } from '..'; /** * Interface holding an entity component pair @@ -46,6 +44,21 @@ export function isRemovedComponent(x: Message): x is RemovedCom return !!x && x.type === 'Component Removed'; } +/** + * Built in events supported by all entities + */ +export type EntityEvents = { + 'initialize': InitializeEvent; + 'preupdate': PreUpdateEvent; + 'postupdate': PostUpdateEvent; +}; + +export const EntityEvents = { + Initialize: 'initialize', + PreUpdate: 'preupdate', + PostUpdate: 'postupdate' +} as const; + /** * An Entity is the base type of anything that can have behavior in Excalibur, they are part of the built in entity component system * @@ -57,11 +70,15 @@ export function isRemovedComponent(x: Message): x is RemovedCom * entity.components.b; // Type ComponentB * ``` */ -export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUpdate { +export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { private static _ID = 0; + /** + * Listen to or emit events for an entity + */ + public events = new EventEmitter(); + constructor(components?: Component[], name?: string) { - super(); this._setName(name); if (components) { for (const component of components) { @@ -90,10 +107,6 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp return this._name; } - public get events(): EventDispatcher { - return this.eventDispatcher; - } - /** * Whether this entity is active, if set to false it will be reclaimed */ @@ -462,7 +475,7 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp public _initialize(engine: Engine) { if (!this.isInitialized) { this.onInitialize(engine); - super.emit('initialize', new InitializeEvent(engine, this)); + this.events.emit('initialize', new InitializeEvent(engine, this)); this._isInitialized = true; } } @@ -474,7 +487,7 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp * @internal */ public _preupdate(engine: Engine, delta: number): void { - this.emit('preupdate', new PreUpdateEvent(engine, delta, this)); + this.events.emit('preupdate', new PreUpdateEvent(engine, delta, this)); this.onPreUpdate(engine, delta); } @@ -485,7 +498,7 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp * @internal */ public _postupdate(engine: Engine, delta: number): void { - this.emit('postupdate', new PostUpdateEvent(engine, delta, this)); + this.events.emit('postupdate', new PostUpdateEvent(engine, delta, this)); this.onPostUpdate(engine, delta); } @@ -533,4 +546,29 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp } this._postupdate(engine, delta); } + + public emit>(eventName: TEventName, event: EntityEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); + } + + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); + } } diff --git a/src/engine/EventEmitter.ts b/src/engine/EventEmitter.ts index 5ee5e9c0..c9d3e6a2 100644 --- a/src/engine/EventEmitter.ts +++ b/src/engine/EventEmitter.ts @@ -12,42 +12,48 @@ export interface Subscription { /** * Excalibur's typed event emitter, this allows events to be sent with any string to Type mapping */ -export class EventEmitter { +export class EventEmitter { private _paused = false; private _listeners: Record[]> = {}; private _listenersOnce: Record[]> = {}; private _pipes: EventEmitter[] = []; - on>(eventName: K, fn: Handler): Subscription; - on(eventName: string, fn: Handler): Subscription; - on | string>(eventName: K, fn: Handler): Subscription { + clear() { + this._listeners = {}; + this._listenersOnce = {}; + this._pipes.length = 0; + } + + on>(eventName: TEventName, handler: Handler): Subscription; + on(eventName: string, handler: Handler): Subscription; + on | string>(eventName: TEventName, handler: Handler): Subscription { this._listeners[eventName] = this._listeners[eventName] ?? []; - this._listeners[eventName].push(fn); + this._listeners[eventName].push(handler); return { - close: () => this.off(eventName, fn) + close: () => this.off(eventName, handler) }; } - once>(eventName: K, fn: Handler): Subscription; - once(eventName: string, fn: Handler): Subscription; - once | string>(eventName: K, fn: Handler): Subscription { + once>(eventName: TEventName, handler: Handler): Subscription; + once(eventName: string, handler: Handler): Subscription; + once | string>(eventName: TEventName, handler: Handler): Subscription { this._listenersOnce[eventName] = this._listenersOnce[eventName] ?? []; - this._listenersOnce[eventName].push(fn); + this._listenersOnce[eventName].push(handler); return { - close: () => this.off(eventName, fn) + close: () => this.off(eventName, handler) }; } - off>(eventName: K, fn: Handler): void; - off(eventName: string, fn: Handler): void; + off>(eventName: TEventName, handler: Handler): void; + off(eventName: string, handler: Handler): void; off(eventName: string): void; - off | string>(eventName: K, fn?: Handler): void { - if (fn) { - const listenerIndex = this._listeners[eventName]?.indexOf(fn); + off | string>(eventName: TEventName, handler?: Handler): void { + if (handler) { + const listenerIndex = this._listeners[eventName]?.indexOf(handler); if (listenerIndex > -1) { this._listeners[eventName]?.splice(listenerIndex, 1); } - const onceIndex = this._listenersOnce[eventName]?.indexOf(fn); + const onceIndex = this._listenersOnce[eventName]?.indexOf(handler); if (onceIndex > -1) { this._listenersOnce[eventName]?.splice(onceIndex, 1); } @@ -56,9 +62,9 @@ export class EventEmitter { } } - emit>(eventName: K, event: T[K]): void; + emit>(eventName: TEventName, event: TEventMap[TEventName]): void; emit(eventName: string, event?: any): void; - emit | string>(eventName: K, event?: T[K]): void { + emit | string>(eventName: TEventName, event?: TEventMap[TEventName]): void { if (this._paused) { return; } diff --git a/src/engine/Events.ts b/src/engine/Events.ts index 22f65eb5..2e55fc58 100644 --- a/src/engine/Events.ts +++ b/src/engine/Events.ts @@ -481,7 +481,7 @@ export class CollisionEndEvent extends GameEvent { /** diff --git a/src/engine/Graphics/Animation.ts b/src/engine/Graphics/Animation.ts index b1cf89b4..b46f3269 100644 --- a/src/engine/Graphics/Animation.ts +++ b/src/engine/Graphics/Animation.ts @@ -94,6 +94,12 @@ export type AnimationEvents = { ended: Animation; }; +export const AnimationEvents = { + Frame: 'frame', + Loop: 'loop', + Ended: 'ended' +}; + export interface FromSpriteSheetOptions { /** * [[SpriteSheet]] to source the animation frames from diff --git a/src/engine/Graphics/OffscreenSystem.ts b/src/engine/Graphics/OffscreenSystem.ts index 1d7e60ff..1d8614bd 100644 --- a/src/engine/Graphics/OffscreenSystem.ts +++ b/src/engine/Graphics/OffscreenSystem.ts @@ -47,12 +47,12 @@ export class OffscreenSystem extends System(); /** * Whether or not to poll for Gamepad input (default: `false`) */ @@ -29,10 +43,6 @@ export class Gamepads extends Class { private _navigator: NavigatorGamepads = navigator; private _minimumConfiguration: GamepadConfiguration = null; - constructor() { - super(); - } - public init() { if (!this.supported) { return; @@ -90,19 +100,31 @@ export class Gamepads extends Class { return axesLength >= this._minimumConfiguration.axis && buttonLength >= this._minimumConfiguration.buttons && pad.connected; } - public on(eventName: Events.connect, handler: (event: GamepadConnectEvent) => void): void; - public on(eventName: Events.disconnect, handler: (event: GamepadDisconnectEvent) => void): void; - public on(eventName: Events.button, handler: (event: GamepadButtonEvent) => void): void; - public on(eventName: Events.axis, handler: (event: GamepadAxisEvent) => void): void; - public on(eventName: string, handler: (event: GameEvent) => void): void; - public on(eventName: string, handler: (event: any) => void): void { + public emit>(eventName: TEventName, event: GamepadEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { this._enableAndUpdate(); // implicitly enable - super.on(eventName, handler); + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); } - public off(eventName: string, handler?: (event: GameEvent) => void) { + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { this._enableAndUpdate(); // implicitly enable - super.off(eventName, handler); + this.events.off(eventName, handler); } /** @@ -121,14 +143,14 @@ export class Gamepads extends Class { const gamepad = this.at(i); // If was connected, but now isn't emit the disconnect event if (gamepad.connected) { - this.eventDispatcher.emit('disconnect', new GamepadDisconnectEvent(i, gamepad)); + this.events.emit('disconnect', new GamepadDisconnectEvent(i, gamepad)); } // Reset connection status gamepad.connected = false; continue; } else { if (!this.at(i).connected && this._isGamepadValid(gamepads[i])) { - this.eventDispatcher.emit('connect', new GamepadConnectEvent(i, this.at(i))); + this.events.emit('connect', new GamepadConnectEvent(i, this.at(i))); } // Set connection status this.at(i).connected = true; @@ -157,7 +179,7 @@ export class Gamepads extends Class { if (value !== this._oldPads[i].getButton(bi)) { if (gamepads[i].buttons[bi].pressed) { this.at(i).updateButton(bi, value); - this.at(i).eventDispatcher.emit('button', new GamepadButtonEvent(bi, value, this.at(i))); + this.at(i).events.emit('button', new GamepadButtonEvent(bi, value, this.at(i))); } else { this.at(i).updateButton(bi, 0); } @@ -173,7 +195,7 @@ export class Gamepads extends Class { value = gamepads[i].axes[ai]; if (value !== this._oldPads[i].getAxes(ai)) { this.at(i).updateAxes(ai, value); - this.at(i).eventDispatcher.emit('axis', new GamepadAxisEvent(ai, value, this.at(i))); + this.at(i).events.emit('axis', new GamepadAxisEvent(ai, value, this.at(i))); } } } @@ -256,7 +278,8 @@ export class Gamepads extends Class { * Gamepad holds state information for a connected controller. See [[Gamepads]] * for more information on handling controller input. */ -export class Gamepad extends Class { +export class Gamepad { + public events = new EventEmitter(); public connected = false; public navigatorGamepad: NavigatorGamepad; private _axes: number[] = new Array(4); @@ -265,8 +288,6 @@ export class Gamepad extends Class { private _buttonsDown: number[] = new Array(16); constructor() { - super(); - for (let i = 0; i < this._buttons.length; i++) { this._buttons[i] = 0; } @@ -358,6 +379,31 @@ export class Gamepad extends Class { public updateAxes(axesIndex: number, value: number) { this._axes[axesIndex] = value; } + + public emit>(eventName: TEventName, event: GamepadEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); + } + + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); + } } /** diff --git a/src/engine/Input/Keyboard.ts b/src/engine/Input/Keyboard.ts index f67b4b74..f6e856f1 100644 --- a/src/engine/Input/Keyboard.ts +++ b/src/engine/Input/Keyboard.ts @@ -1,7 +1,7 @@ import { Logger } from '../Util/Log'; -import { Class } from '../Class'; import * as Events from '../Events'; import { isCrossOriginIframe } from '../Util/IFrame'; +import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter'; /** * Enum representing physical input key codes @@ -195,10 +195,23 @@ export interface KeyboardInitOptions { grabWindowFocus?: boolean } +export type KeyEvents = { + press: KeyEvent, + hold: KeyEvent, + release: KeyEvent +}; + +export const KeyEvents = { + Press: 'press', + Hold: 'hold', + Release: 'release' +}; + /** * Provides keyboard support for Excalibur. */ -export class Keyboard extends Class { +export class Keyboard { + public events = new EventEmitter(); /** * Keys that are currently held down */ @@ -212,16 +225,29 @@ export class Keyboard extends Class { */ private _keysDown: Keys[] = []; - constructor() { - super(); + public emit>(eventName: TEventName, event: KeyEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); } - public on(eventName: Events.press, handler: (event: KeyEvent) => void): void; - public on(eventName: Events.release, handler: (event: KeyEvent) => void): void; - public on(eventName: Events.hold, handler: (event: KeyEvent) => void): void; - public on(eventName: string, handler: (event: Events.GameEvent) => void): void; - public on(eventName: string, handler: (event: any) => void): void { - super.on(eventName, handler); + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } /** @@ -260,8 +286,8 @@ export class Keyboard extends Class { private _releaseAllKeys = (ev: KeyboardEvent) => { for (const code of this._keys) { const keyEvent = new KeyEvent(code, ev.key, ev); - this.eventDispatcher.emit('up', keyEvent); - this.eventDispatcher.emit('release', keyEvent); + this.events.emit('up', keyEvent); + this.events.emit('release', keyEvent); } this._keysUp = Array.from((new Set(this._keys.concat(this._keysUp)))); this._keys.length = 0; @@ -279,8 +305,8 @@ export class Keyboard extends Class { this._keys.push(code); this._keysDown.push(code); const keyEvent = new KeyEvent(code, ev.key, ev); - this.eventDispatcher.emit('down', keyEvent); - this.eventDispatcher.emit('press', keyEvent); + this.events.emit('down', keyEvent); + this.events.emit('press', keyEvent); } }; @@ -292,8 +318,8 @@ export class Keyboard extends Class { const keyEvent = new KeyEvent(code, ev.key, ev); // alias the old api, we may want to deprecate this in the future - this.eventDispatcher.emit('up', keyEvent); - this.eventDispatcher.emit('release', keyEvent); + this.events.emit('up', keyEvent); + this.events.emit('release', keyEvent); // handle macos meta key issue // https://github.com/excaliburjs/Excalibur/issues/2608 @@ -309,7 +335,7 @@ export class Keyboard extends Class { // Emit synthetic "hold" event for (let i = 0; i < this._keys.length; i++) { - this.eventDispatcher.emit('hold', new KeyEvent(this._keys[i])); + this.events.emit('hold', new KeyEvent(this._keys[i])); } } diff --git a/src/engine/Input/PointerAbstraction.ts b/src/engine/Input/PointerAbstraction.ts index aa2ac455..ba298c0b 100644 --- a/src/engine/Input/PointerAbstraction.ts +++ b/src/engine/Input/PointerAbstraction.ts @@ -1,10 +1,10 @@ -import { Class } from '../Class'; import { Vector } from '../Math/vector'; -import { WheelEvent } from './WheelEvent'; import { PointerEvent } from './PointerEvent'; +import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter'; +import { PointerEvents } from './PointerEventReceiver'; -export class PointerAbstraction extends Class { - +export class PointerAbstraction { + public events = new EventEmitter(); /** * The last position on the document this pointer was at. Can be `null` if pointer was never active. */ @@ -21,33 +21,33 @@ export class PointerAbstraction extends Class { public lastWorldPos: Vector = Vector.Zero; constructor() { - super(); this.on('move', this._onPointerMove); this.on('down', this._onPointerDown); } - on(event: 'move', handler: (event: PointerEvent) => void): void; - on(event: 'down', handler: (event: PointerEvent) => void): void; - on(event: 'up', handler: (event: PointerEvent) => void): void; - on(event: 'wheel', handler: (event: WheelEvent) => void): void; - on(event: string, handler: (event: any) => void): void { - super.on(event, handler); + public emit>(eventName: TEventName, event: PointerEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); } - once(event: 'move', handler: (event: PointerEvent) => void): void; - once(event: 'down', handler: (event: PointerEvent) => void): void; - once(event: 'up', handler: (event: PointerEvent) => void): void; - once(event: 'wheel', handler: (event: WheelEvent) => void): void; - once(event: string, handler: (event: any) => void): void { - super.once(event, handler); + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); } - off(event: 'move', handler?: (event: PointerEvent) => void): void; - off(event: 'down', handler?: (event: PointerEvent) => void): void; - off(event: 'up', handler?: (event: PointerEvent) => void): void; - off(event: 'wheel', handler?: (event: WheelEvent) => void): void; - off(event: string, handler?: (event: any) => void): void { - super.off(event, handler); + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } private _onPointerMove = (ev: PointerEvent): void => { diff --git a/src/engine/Input/PointerEventReceiver.ts b/src/engine/Input/PointerEventReceiver.ts index 5bf50523..5fbeb558 100644 --- a/src/engine/Input/PointerEventReceiver.ts +++ b/src/engine/Input/PointerEventReceiver.ts @@ -1,4 +1,3 @@ -import { Class } from '../Class'; import { Engine, ScrollPreventionMode } from '../Engine'; import { GlobalCoordinates } from '../Math/global-coordinates'; import { vec, Vector } from '../Math/vector'; @@ -13,6 +12,7 @@ import { PointerButton } from './PointerButton'; import { fail } from '../Util/Util'; import { PointerType } from './PointerType'; import { isCrossOriginIframe } from '../Util/IFrame'; +import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter'; export type NativePointerEvent = globalThis.PointerEvent; @@ -20,6 +20,20 @@ export type NativeMouseEvent = globalThis.MouseEvent; export type NativeTouchEvent = globalThis.TouchEvent; export type NativeWheelEvent = globalThis.WheelEvent; +export type PointerEvents = { + move: PointerEvent, + down: PointerEvent, + up: PointerEvent, + wheel: WheelEvent +} + +export const PointerEvents = { + Move: 'move', + Down: 'down', + Up: 'up', + Wheel: 'wheel' +}; + /** * Is this event a native touch event? */ @@ -43,7 +57,8 @@ export interface PointerInitOptions { /** * The PointerEventProcessor is responsible for collecting all the events from the canvas and transforming them into GlobalCoordinates */ -export class PointerEventReceiver extends Class { +export class PointerEventReceiver { + public events = new EventEmitter(); public primary: PointerAbstraction = new PointerAbstraction(); private _activeNativePointerIdsToNormalized = new Map(); @@ -59,9 +74,7 @@ export class PointerEventReceiver extends Class { public currentFrameCancel: PointerEvent[] = []; public currentFrameWheel: WheelEvent[] = []; - constructor(public readonly target: GlobalEventHandlers & EventTarget, public engine: Engine) { - super(); - } + constructor(public readonly target: GlobalEventHandlers & EventTarget, public engine: Engine) {} /** * Creates a new PointerEventReceiver with a new target and engine while preserving existing pointer event @@ -135,28 +148,29 @@ export class PointerEventReceiver extends Class { return !this.isDown(pointerId) && this.wasDown(pointerId); } - on(event: 'move', handler: (event: PointerEvent) => void): void; - on(event: 'down', handler: (event: PointerEvent) => void): void; - on(event: 'up', handler: (event: PointerEvent) => void): void; - on(event: 'wheel', handler: (event: WheelEvent) => void): void; - on(event: string, handler: (event: any) => void): void { - super.on(event, handler); + public emit>(eventName: TEventName, event: PointerEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); } - once(event: 'move', handler: (event: PointerEvent) => void): void; - once(event: 'down', handler: (event: PointerEvent) => void): void; - once(event: 'up', handler: (event: PointerEvent) => void): void; - once(event: 'wheel', handler: (event: WheelEvent) => void): void; - once(event: string, handler: (event: any) => void): void { - super.once(event, handler); + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); } - off(event: 'move', handler?: (event: PointerEvent) => void): void; - off(event: 'down', handler?: (event: PointerEvent) => void): void; - off(event: 'up', handler?: (event: PointerEvent) => void): void; - off(event: 'wheel', handler?: (event: WheelEvent) => void): void; - off(event: string, handler?: (event: any) => void): void { - super.off(event, handler); + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } /** diff --git a/src/engine/Interfaces/Evented.ts b/src/engine/Interfaces/Evented.ts index 18894a28..77907033 100644 --- a/src/engine/Interfaces/Evented.ts +++ b/src/engine/Interfaces/Evented.ts @@ -1,4 +1,4 @@ -import { GameEvent } from '../Events'; +import { Handler } from '../EventEmitter'; export interface Eventable { /** @@ -6,14 +6,14 @@ export interface Eventable { * @param eventName The name of the event to publish * @param event Optionally pass an event data object to the handler */ - emit(eventName: string, event: GameEvent): void; + emit(eventName: string, event: any): void; /** * Subscribe an event handler to a particular event name, multiple handlers per event name are allowed. * @param eventName The name of the event to subscribe to * @param handler The handler callback to fire on this event */ - on(eventName: string, handler: (event: GameEvent) => void): void; + on(eventName: string, handler: Handler): void; /** * Unsubscribe an event handler(s) from an event. If a specific handler @@ -23,7 +23,7 @@ export interface Eventable { * @param eventName The name of the event to unsubscribe * @param handler Optionally the specific handler to unsubscribe */ - off(eventName: string, handler?: (event: GameEvent) => void): void; + off(eventName: string, handler?: Handler): void; /** * Once listens to an event once then auto unsubscribes from that event @@ -31,5 +31,5 @@ export interface Eventable { * @param eventName The name of the event to subscribe to once * @param handler The handler of the event that will be auto unsubscribed */ - once(eventName: string, handler: (event: GameEvent) => void): void; + once(eventName: string, handler: Handler): void; } diff --git a/src/engine/Loader.ts b/src/engine/Loader.ts index ab0ece86..5cd12cbe 100644 --- a/src/engine/Loader.ts +++ b/src/engine/Loader.ts @@ -2,7 +2,6 @@ import { Color } from './Color'; import { WebAudio } from './Util/WebAudio'; import { Engine } from './Engine'; import { Loadable } from './Interfaces/Loadable'; -import { Class } from './Class'; import * as DrawUtil from './Util/DrawUtil'; import logoImg from './Loader.logo.png'; @@ -14,6 +13,15 @@ import { ImageFiltering } from './Graphics/Filtering'; import { clamp } from './Math/util'; import { Sound } from './Resources/Sound/Sound'; import { Future } from './Util/Future'; +import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; + +export type LoaderEvents = { + // Add event types here +} + +export const LoaderEvents = { + // Add event types here +}; /** * Pre-loading assets @@ -80,7 +88,8 @@ import { Future } from './Util/Future'; * engine.start(loader).then(() => {}); * ``` */ -export class Loader extends Class implements Loadable[]> { +export class Loader implements Loadable[]> { + public events = new EventEmitter(); public canvas: Canvas = new Canvas({ filtering: ImageFiltering.Blended, smoothing: true, @@ -198,8 +207,6 @@ export class Loader extends Class implements Loadable[]> { * @param loadables Optionally provide the list of resources you want to load at constructor time */ constructor(loadables?: Loadable[]) { - super(); - if (loadables) { this.addResources(loadables); } @@ -449,4 +456,29 @@ export class Loader extends Class implements Loadable[]> { ); this._engine.setAntialiasing(oldAntialias); } + + public emit>(eventName: TEventName, event: LoaderEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); + } + + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); + } } diff --git a/src/engine/Resources/Resource.ts b/src/engine/Resources/Resource.ts index 6d7c60b5..99ac9371 100644 --- a/src/engine/Resources/Resource.ts +++ b/src/engine/Resources/Resource.ts @@ -1,6 +1,22 @@ import { Loadable } from '../Interfaces/Loadable'; import { Logger } from '../Util/Log'; -import { EventDispatcher } from '../EventDispatcher'; +import { EventEmitter } from '../EventEmitter'; + +export type ResourceEvents = { + complete: any, + load: ProgressEvent, + loadstart: ProgressEvent, + progress: ProgressEvent, + error: ProgressEvent +} + +export const ResourceEvents = { + Complete: 'complete', + Load: 'load', + LoadStart: 'loadstart', + Progress: 'progress', + Error: 'error' +}; /** * The [[Resource]] type allows games built in Excalibur to load generic resources. @@ -9,7 +25,7 @@ import { EventDispatcher } from '../EventDispatcher'; export class Resource implements Loadable { public data: T = null; public logger: Logger = Logger.getInstance(); - public events: EventDispatcher = new EventDispatcher(); + public events = new EventEmitter(); /** * @param path Path to the remote resource diff --git a/src/engine/Resources/Sound/Sound.ts b/src/engine/Resources/Sound/Sound.ts index 396754bb..0b6c4dbb 100644 --- a/src/engine/Resources/Sound/Sound.ts +++ b/src/engine/Resources/Sound/Sound.ts @@ -8,14 +8,35 @@ import { NativeSoundEvent, NativeSoundProcessedEvent } from '../../Events/MediaE import { canPlayFile } from '../../Util/Sound'; import { Loadable } from '../../Interfaces/Index'; import { Logger } from '../../Util/Log'; -import { Class } from '../../Class'; +import { EventEmitter, EventKey, Handler, Subscription } from '../../EventEmitter'; + +export type SoundEvents = { + volumechange: NativeSoundEvent, + processed: NativeSoundProcessedEvent, + pause: NativeSoundEvent, + stop: NativeSoundEvent, + playbackend: NativeSoundEvent, + resume: NativeSoundEvent, + playbackstart: NativeSoundEvent +} + +export const SoundEvents = { + VolumeChange: 'volumechange', + Processed: 'processed', + Pause: 'pause', + Stop: 'stop', + PlaybackEnd: 'playbackend', + Resume: 'resume', + PlaybackStart: 'playbackstart' +}; /** * The [[Sound]] object allows games built in Excalibur to load audio * components, from soundtracks to sound effects. [[Sound]] is an [[Loadable]] * which means it can be passed to a [[Loader]] to pre-load before a game or level. */ -export class Sound extends Class implements Audio, Loadable { +export class Sound implements Audio, Loadable { + public events = new EventEmitter(); public logger: Logger = Logger.getInstance(); public data: AudioBuffer; private _resource: Resource; @@ -43,7 +64,7 @@ export class Sound extends Class implements Audio, Loadable { track.volume = this._volume; } - this.emit('volumechange', new NativeSoundEvent(this)); + this.events.emit('volumechange', new NativeSoundEvent(this)); this.logger.debug('Set loop for all instances of sound', this.path, 'to', this._volume); } @@ -111,7 +132,6 @@ export class Sound extends Class implements Audio, Loadable { * @param paths A list of audio sources (clip.wav, clip.mp3, clip.ogg) for this audio clip. This is done for browser compatibility. */ constructor(...paths: string[]) { - super(); this._resource = new Resource('', ExResponse.type.arraybuffer); /** * Chrome : MP3, WAV, Ogg @@ -144,7 +164,7 @@ export class Sound extends Class implements Audio, Loadable { const arraybuffer = await this._resource.load(); const audiobuffer = await this.decodeAudio(arraybuffer.slice(0)); this._duration = this._duration ?? audiobuffer?.duration ?? undefined; - this.emit('processed', new NativeSoundProcessedEvent(this, audiobuffer)); + this.events.emit('processed', new NativeSoundProcessedEvent(this, audiobuffer)); return this.data = audiobuffer; } @@ -245,7 +265,7 @@ export class Sound extends Class implements Audio, Loadable { track.pause(); } - this.emit('pause', new NativeSoundEvent(this)); + this.events.emit('pause', new NativeSoundEvent(this)); this.logger.debug('Paused all instances of sound', this.path); } @@ -258,7 +278,7 @@ export class Sound extends Class implements Audio, Loadable { track.stop(); } - this.emit('stop', new NativeSoundEvent(this)); + this.events.emit('stop', new NativeSoundEvent(this)); this._tracks.length = 0; this.logger.debug('Stopped all instances of sound', this.path); @@ -316,13 +336,13 @@ export class Sound extends Class implements Audio, Loadable { // ensure we resume *current* tracks (if paused) for (const track of this._tracks) { resumed.push(track.play().then(() => { - this.emit('playbackend', new NativeSoundEvent(this, track as WebAudioInstance)); + this.events.emit('playbackend', new NativeSoundEvent(this, track as WebAudioInstance)); this._tracks.splice(this.getTrackId(track), 1); return true; })); } - this.emit('resume', new NativeSoundEvent(this)); + this.events.emit('resume', new NativeSoundEvent(this)); this.logger.debug('Resuming paused instances for sound', this.path, this._tracks); // resolve when resumed tracks are done @@ -338,12 +358,12 @@ export class Sound extends Class implements Audio, Loadable { const track = await this._getTrackInstance(this.data); const complete = await track.play(() => { - this.emit('playbackstart', new NativeSoundEvent(this, track)); + this.events.emit('playbackstart', new NativeSoundEvent(this, track)); this.logger.debug('Playing new instance for sound', this.path); }); // when done, remove track - this.emit('playbackend', new NativeSoundEvent(this, track)); + this.events.emit('playbackend', new NativeSoundEvent(this, track)); this._tracks.splice(this.getTrackId(track), 1); return complete; @@ -361,4 +381,29 @@ export class Sound extends Class implements Audio, Loadable { return newTrack; } + + public emit>(eventName: TEventName, event: SoundEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); + } + + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); + } } diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts index 112f2edb..d240372b 100644 --- a/src/engine/Scene.ts +++ b/src/engine/Scene.ts @@ -8,8 +8,7 @@ import { PreDrawEvent, PostDrawEvent, PreDebugDrawEvent, - PostDebugDrawEvent, - GameEvent + PostDebugDrawEvent } from './Events'; import { Logger } from './Util/Log'; import { Timer } from './Timer'; @@ -17,10 +16,8 @@ import { Engine } from './Engine'; import { TileMap } from './TileMap'; import { Camera } from './Camera'; import { Actor } from './Actor'; -import { Class } from './Class'; import { CanInitialize, CanActivate, CanDeactivate, CanUpdate, CanDraw, SceneActivationContext } from './Interfaces/LifecycleEvents'; import * as Util from './Util/Util'; -import * as Events from './Events'; import { Trigger } from './Trigger'; import { SystemType } from './EntityComponentSystem/System'; import { World } from './EntityComponentSystem/World'; @@ -35,6 +32,32 @@ import { IsometricEntitySystem } from './TileMap/IsometricEntitySystem'; import { OffscreenSystem } from './Graphics/OffscreenSystem'; import { ExcaliburGraphicsContext } from './Graphics'; import { PhysicsWorld } from './Collision/PhysicsWorld'; +import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; + +export type SceneEvents = { + initialize: InitializeEvent, + activate: ActivateEvent, + deactivate: DeactivateEvent, + preupdate: PreUpdateEvent, + postupdate: PostUpdateEvent, + predraw: PreDrawEvent, + postdraw: PostDrawEvent, + predebugdraw: PreDebugDrawEvent, + postdebugdraw: PostDebugDrawEvent +} + +export const SceneEvents = { + Initialize: 'initialize', + Activate: 'activate', + Deactivate: 'deactivate', + PreUpdate: 'preupdate', + PostUpdate: 'postupdate', + PreDraw: 'predraw', + PostDraw: 'postdraw', + PreDebugDraw: 'predebugdraw', + PostDebugDraw: 'postdebugdraw' +}; + /** * [[Actor|Actors]] are composed together into groupings called Scenes in * Excalibur. The metaphor models the same idea behind real world @@ -43,9 +66,10 @@ import { PhysicsWorld } from './Collision/PhysicsWorld'; * Typical usages of a scene include: levels, menus, loading screens, etc. */ export class Scene - extends Class - implements CanInitialize, CanActivate, CanDeactivate, CanUpdate, CanDraw { +implements CanInitialize, CanActivate, CanDeactivate, CanUpdate, CanDraw { private _logger: Logger = Logger.getInstance(); + public events = new EventEmitter(); + /** * Gets or sets the current camera for the scene */ @@ -111,7 +135,6 @@ export class Scene private _cancelQueue: Timer[] = []; constructor() { - super(); // Initialize systems // Update @@ -126,46 +149,29 @@ export class Scene this.world.add(new DebugSystem()); } - public on(eventName: Events.initialize, handler: (event: InitializeEvent) => void): void; - public on(eventName: Events.activate, handler: (event: ActivateEvent) => void): void; - public on(eventName: Events.deactivate, handler: (event: DeactivateEvent) => void): void; - public on(eventName: Events.preupdate, handler: (event: PreUpdateEvent) => void): void; - public on(eventName: Events.postupdate, handler: (event: PostUpdateEvent) => void): void; - public on(eventName: Events.predraw, handler: (event: PreDrawEvent) => void): void; - public on(eventName: Events.postdraw, handler: (event: PostDrawEvent) => void): void; - public on(eventName: Events.predebugdraw, handler: (event: PreDebugDrawEvent) => void): void; - public on(eventName: Events.postdebugdraw, handler: (event: PostDebugDrawEvent) => void): void; - public on(eventName: string, handler: (event: GameEvent) => void): void; - public on(eventName: string, handler: (event: any) => void): void { - super.on(eventName, handler); - } - - public once(eventName: Events.initialize, handler: (event: InitializeEvent) => void): void; - public once(eventName: Events.activate, handler: (event: ActivateEvent) => void): void; - public once(eventName: Events.deactivate, handler: (event: DeactivateEvent) => void): void; - public once(eventName: Events.preupdate, handler: (event: PreUpdateEvent) => void): void; - public once(eventName: Events.postupdate, handler: (event: PostUpdateEvent) => void): void; - public once(eventName: Events.predraw, handler: (event: PreDrawEvent) => void): void; - public once(eventName: Events.postdraw, handler: (event: PostDrawEvent) => void): void; - public once(eventName: Events.predebugdraw, handler: (event: PreDebugDrawEvent) => void): void; - public once(eventName: Events.postdebugdraw, handler: (event: PostDebugDrawEvent) => void): void; - public once(eventName: string, handler: (event: GameEvent) => void): void; - public once(eventName: string, handler: (event: any) => void): void { - super.once(eventName, handler); - } - - public off(eventName: Events.initialize, handler?: (event: InitializeEvent) => void): void; - public off(eventName: Events.activate, handler?: (event: ActivateEvent) => void): void; - public off(eventName: Events.deactivate, handler?: (event: DeactivateEvent) => void): void; - public off(eventName: Events.preupdate, handler?: (event: PreUpdateEvent) => void): void; - public off(eventName: Events.postupdate, handler?: (event: PostUpdateEvent) => void): void; - public off(eventName: Events.predraw, handler?: (event: PreDrawEvent) => void): void; - public off(eventName: Events.postdraw, handler?: (event: PostDrawEvent) => void): void; - public off(eventName: Events.predebugdraw, handler?: (event: PreDebugDrawEvent) => void): void; - public off(eventName: Events.postdebugdraw, handler?: (event: PostDebugDrawEvent) => void): void; - public off(eventName: string, handler?: (event: GameEvent) => void): void; - public off(eventName: string, handler?: (event: any) => void): void { - super.off(eventName, handler); + public emit>(eventName: TEventName, event: SceneEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); + } + + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } /** @@ -267,7 +273,7 @@ export class Scene this._initializeChildren(); this._logger.debug('Scene.onInitialize', this, engine); - this.eventDispatcher.emit('initialize', new InitializeEvent(engine, this)); + this.events.emit('initialize', new InitializeEvent(engine, this)); this._isInitialized = true; } } diff --git a/src/engine/TileMap/TileMap.ts b/src/engine/TileMap/TileMap.ts index 3aa071a7..f450b8f4 100644 --- a/src/engine/TileMap/TileMap.ts +++ b/src/engine/TileMap/TileMap.ts @@ -2,8 +2,7 @@ import { BoundingBox } from '../Collision/BoundingBox'; import { Engine } from '../Engine'; import { Vector, vec } from '../Math/vector'; import { Logger } from '../Util/Log'; -import * as Events from '../Events'; -import { Entity } from '../EntityComponentSystem/Entity'; +import { Entity, EntityEvents } from '../EntityComponentSystem/Entity'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { BodyComponent } from '../Collision/BodyComponent'; import { CollisionType } from '../Collision/CollisionType'; @@ -16,6 +15,8 @@ import { CompositeCollider } from '../Collision/Colliders/CompositeCollider'; import { Color } from '../Color'; import { DebugGraphicsComponent } from '../Graphics/DebugGraphicsComponent'; import { Collider } from '../Collision/Colliders/Collider'; +import { PostDrawEvent, PostUpdateEvent, PreDrawEvent, PreUpdateEvent } from '../Events'; +import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter'; export interface TileMapOptions { /** @@ -49,12 +50,27 @@ export interface TileMapOptions { renderFromTopOfGraphic?: boolean; } +export type TileMapEvents = EntityEvents & { + preupdate: PreUpdateEvent; + postupdate: PostUpdateEvent; + predraw: PreDrawEvent; + postdraw: PostDrawEvent +} + +export const TileMapEvents = { + PreUpdate: 'preupdate', + PostUpdate: 'postupdate', + PreDraw: 'predraw', + PostDraw: 'postdraw' +}; + /** * The TileMap provides a mechanism for doing flat 2D tiles rendered in a grid. * * TileMaps are useful for top down or side scrolling grid oriented games. */ export class TileMap extends Entity { + public events = new EventEmitter(); private _token = 0; private _engine: Engine; @@ -157,13 +173,29 @@ export class TileMap extends Entity { this._motion.vel = val; } - public on(eventName: Events.preupdate, handler: (event: Events.PreUpdateEvent) => void): void; - public on(eventName: Events.postupdate, handler: (event: Events.PostUpdateEvent) => void): void; - public on(eventName: Events.predraw, handler: (event: Events.PreDrawEvent) => void): void; - public on(eventName: Events.postdraw, handler: (event: Events.PostDrawEvent) => void): void; - public on(eventName: string, handler: (event: Events.GameEvent) => void): void; - public on(eventName: string, handler: (event: any) => void): void { - super.on(eventName, handler); + public emit>(eventName: TEventName, event: TileMapEvents[TEventName]): void; + public emit(eventName: string, event?: any): void; + public emit | string>(eventName: TEventName, event?: any): void { + this.events.emit(eventName, event); + } + + public on>(eventName: TEventName, handler: Handler): Subscription; + public on(eventName: string, handler: Handler): Subscription; + public on | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.on(eventName, handler); + } + + public once>(eventName: TEventName, handler: Handler): Subscription; + public once(eventName: string, handler: Handler): Subscription; + public once | string>(eventName: TEventName, handler: Handler): Subscription { + return this.events.once(eventName, handler); + } + + public off>(eventName: TEventName, handler: Handler): void; + public off(eventName: string, handler: Handler): void; + public off(eventName: string): void; + public off | string>(eventName: TEventName, handler?: Handler): void { + this.events.off(eventName, handler); } @@ -353,7 +385,7 @@ export class TileMap extends Entity { public update(engine: Engine, delta: number) { this.onPreUpdate(engine, delta); - this.emit('preupdate', new Events.PreUpdateEvent(engine, delta, this)); + this.emit('preupdate', new PreUpdateEvent(engine, delta, this)); if (!this._oldPos.equals(this.pos) || !this._oldScale.equals(this.scale)) { this.flagCollidersDirty(); @@ -370,7 +402,7 @@ export class TileMap extends Entity { this.scale.clone(this._oldScale); this._transform.pos = this.pos; this.onPostUpdate(engine, delta); - this.emit('postupdate', new Events.PostUpdateEvent(engine, delta, this)); + this.emit('postupdate', new PostUpdateEvent(engine, delta, this)); } /** @@ -379,7 +411,7 @@ export class TileMap extends Entity { * @param delta The number of milliseconds since the last draw */ public draw(ctx: ExcaliburGraphicsContext, delta: number): void { - this.emit('predraw', new Events.PreDrawEvent(ctx as any, delta, this)); // TODO fix event + this.emit('predraw', new PreDrawEvent(ctx as any, delta, this)); // TODO fix event let worldBounds = this._engine.screen.getWorldBounds(); // TODO can we trim this down by using world bounds? let x = 0; @@ -426,7 +458,7 @@ export class TileMap extends Entity { y = 0; } - this.emit('postdraw', new Events.PostDrawEvent(ctx as any, delta, this)); + this.emit('postdraw', new PostDrawEvent(ctx as any, delta, this)); } public debug(gfx: ExcaliburGraphicsContext) { diff --git a/src/engine/Trigger.ts b/src/engine/Trigger.ts index ba582890..f90de9a3 100644 --- a/src/engine/Trigger.ts +++ b/src/engine/Trigger.ts @@ -1,13 +1,23 @@ import { Engine } from './Engine'; -import { EventDispatcher } from './EventDispatcher'; import { Vector } from './Math/vector'; import { ExitTriggerEvent, EnterTriggerEvent, CollisionEndEvent, CollisionStartEvent } from './Events'; import { CollisionType } from './Collision/CollisionType'; import { Entity } from './EntityComponentSystem'; -import { Actor } from './Actor'; +import { Actor, ActorEvents } from './Actor'; +import { EventEmitter } from './EventEmitter'; + +export type TriggerEvents = ActorEvents & { + exittrigger: ExitTriggerEvent, + entertrigger: EnterTriggerEvent +} + +export const TriggerEvents = { + ExitTrigger: 'exittrigger', + EnterTrigger: 'entertrigger' +}; /** - * ITriggerOptions + * TriggerOptions */ export interface TriggerOptions { // position of the trigger @@ -46,6 +56,7 @@ const triggerDefaults: Partial = { * are invisible, and can only be seen when [[Trigger.visible]] is set to `true`. */ export class Trigger extends Actor { + public events = new EventEmitter(); private _target: Entity; /** * Action to fire when triggered by collision @@ -83,7 +94,6 @@ export class Trigger extends Actor { this.graphics.visible = opts.visible; this.body.collisionType = CollisionType.Passive; - this.eventDispatcher = new EventDispatcher(); this.events.on('collisionstart', (evt: CollisionStartEvent) => { if (this.filter(evt.other)) { diff --git a/src/engine/index.ts b/src/engine/index.ts index c057baa9..d91ac8b2 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -15,7 +15,6 @@ export * from './Screen'; export { Actor, ActorArgs } from './Actor'; export * from './Math/Index'; export * from './Camera'; -export * from './Class'; export * from './Configurable'; export * from './Debug/index'; export * from './EventDispatcher'; diff --git a/src/spec/ColliderComponentSpec.ts b/src/spec/ColliderComponentSpec.ts index 7f54ed12..b5b387ea 100644 --- a/src/spec/ColliderComponentSpec.ts +++ b/src/spec/ColliderComponentSpec.ts @@ -89,7 +89,7 @@ describe('A ColliderComponent', () => { it('clear a collider', () => { const collider = ex.Shape.Circle(50); const comp = new ex.ColliderComponent(collider); - spyOn(comp.events, 'unwire'); + spyOn(collider.events, 'unpipe'); spyOn(comp.$colliderRemoved, 'notifyAll'); const e = new ex.Entity(); e.addComponent(comp); @@ -98,7 +98,7 @@ describe('A ColliderComponent', () => { comp.clear(); expect(comp.get()).toBeNull(); - expect(comp.events.unwire).toHaveBeenCalled(); + expect(collider.events.unpipe).toHaveBeenCalled(); expect(comp.$colliderRemoved.notifyAll).toHaveBeenCalled(); expect(collider.owner).toBeNull(); }); diff --git a/src/spec/OffscreenSystemSpec.ts b/src/spec/OffscreenSystemSpec.ts index 1fe4b250..e270b6de 100644 --- a/src/spec/OffscreenSystemSpec.ts +++ b/src/spec/OffscreenSystemSpec.ts @@ -46,8 +46,8 @@ describe('The OffscreenSystem', () => { const offscreenSpy = jasmine.createSpy('offscreenSpy'); const onscreenSpy = jasmine.createSpy('onscreenSpy'); - offscreen.eventDispatcher.on('enterviewport', onscreenSpy); - offscreen.eventDispatcher.on('exitviewport', offscreenSpy); + offscreen.events.on('enterviewport', onscreenSpy); + offscreen.events.on('exitviewport', offscreenSpy); // Should be offscreen sut.update([offscreen]); -- 2.51.2