diff --git a/CHANGELOG.md b/CHANGELOG.md index b40c21502..fb0433244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,23 @@ This project adheres to [Semantic Versioning](http://semver.org/). loader: boot, }); ``` + - Scene specific input API so that you can add input handlers that only fire when a scene is active! + ```typescript + class SceneWithInput extends ex.Scene { + onInitialize(engine: ex.Engine): void { + this.input.pointers.on('down', () => { + console.log('pointer down from scene1'); + }); + } + } + class OtherSceneWithInput extends ex.Scene { + onInitialize(engine: ex.Engine): void { + this.input.pointers.on('down', () => { + console.log('pointer down from scene2'); + }); + } + } + ``` ### Fixed diff --git a/sandbox/tests/router/index.ts b/sandbox/tests/router/index.ts index 9b9371858..a326a4723 100644 --- a/sandbox/tests/router/index.ts +++ b/sandbox/tests/router/index.ts @@ -14,6 +14,13 @@ scene2.add(new ex.Label({ z: 99 })) +class MyLoader extends ex.DefaultLoader { + onDraw(ctx: CanvasRenderingContext2D): void { + super.onDraw(ctx); + console.log(this.progress); + } +} + class MyCustomScene extends ex.Scene { onTransition(direction: "in" | "out") { return new ex.FadeInOut({ @@ -48,7 +55,7 @@ let scenes = { }, scene2: { scene: scene2, - loader: ex.DefaultLoader, + loader: MyLoader, transitions: { out: new ex.FadeInOut({duration: 500, direction: 'out'}), in: new ex.CrossFade({duration: 2500, direction: 'in', blockInput: true}) @@ -108,7 +115,7 @@ scene2.add(new ex.Actor({ color: ex.Color.Blue })); -var boot = new ex.Loader(); +var boot = new ex.Loader() as ex.Loader; const image1 = new ex.ImageSource('./spritefont.png?=1'); const image2 = new ex.ImageSource('./spritefont.png?=2'); const image3 = new ex.ImageSource('./spritefont.png?=3'); diff --git a/sandbox/tests/scene-input/index.html b/sandbox/tests/scene-input/index.html new file mode 100644 index 000000000..7f7fcbbc1 --- /dev/null +++ b/sandbox/tests/scene-input/index.html @@ -0,0 +1,12 @@ + + + + + + Scene Input + + + + + + \ No newline at end of file diff --git a/sandbox/tests/scene-input/index.ts b/sandbox/tests/scene-input/index.ts new file mode 100644 index 000000000..cf1ae3a28 --- /dev/null +++ b/sandbox/tests/scene-input/index.ts @@ -0,0 +1,69 @@ +/// + +const font = new ex.Font({ + size: 48, + family: 'sans-serif', + baseAlign: ex.BaseAlign.Top +}); + +class SceneWithInput extends ex.Scene { + onInitialize(engine: ex.Engine): void { + this.add(new ex.Label({ + pos: ex.vec(200, 200), + text: 'Scene 1', + font + })); + + this.add(new ex.Actor({ + pos: ex.vec(400, 400), + width: 200, + height: 200, + color: ex.Color.Red + })); + + this.input.pointers.on('down', () => { + console.log('pointer down from scene1'); + }); + } +} +class OtherSceneWithInput extends ex.Scene { + onInitialize(engine: ex.Engine): void { + this.add(new ex.Label({ + pos: ex.vec(200, 200), + text: 'Scene 2', + font + })); + + this.add(new ex.Actor({ + pos: ex.vec(400, 400), + width: 200, + height: 200, + color: ex.Color.Violet + })); + + this.input.pointers.on('down', () => { + console.log('pointer down from scene2'); + }); + } +} + +var engineWithInput = new ex.Engine({ + width: 800, + height: 800, + scenes: { + scene1: { scene: SceneWithInput, transitions: {in: new ex.CrossFade({duration: 1000, blockInput: true})}}, + scene2: { scene: OtherSceneWithInput, transitions: {in: new ex.CrossFade({duration: 1000, blockInput: true})}} + } +}); + +engineWithInput.input.pointers.on('down', () => { + console.log('pointer down from engine'); +}); + +engineWithInput.input.keyboard.on('press', e => { + if (e.key === ex.Keys.Space) { + engineWithInput.currentSceneName === 'scene1' ? engineWithInput.goto('scene2') : engineWithInput.goto('scene1'); + } +}) + +engineWithInput.start('scene1'); \ No newline at end of file diff --git a/src/engine/Director/Director.ts b/src/engine/Director/Director.ts index 1658d0ea0..70e8ecc50 100644 --- a/src/engine/Director/Director.ts +++ b/src/engine/Director/Director.ts @@ -372,6 +372,7 @@ export class Director { } const sourceScene = this.currentSceneName; + const engineInputEnabled = this._engine.input?.enabled ?? true; this._isTransitioning = true; const maybeSourceOut = this.getSceneInstance(sourceScene)?.onTransition('out'); @@ -416,7 +417,7 @@ export class Director { await this.playTransition(inTransition); this._emitEvent('navigationend', sourceScene, destinationScene); - this._engine.toggleInputEnabled(true); + this._engine.input?.toggleEnabled(engineInputEnabled); this._isTransitioning = false; } @@ -473,9 +474,16 @@ export class Director { async playTransition(transition: Transition) { if (transition) { this.currentTransition = transition; - this._engine.toggleInputEnabled(!transition.blockInput); + const currentScene = this._engine.currentScene; + const sceneInputEnabled = currentScene.input?.enabled ?? true; + + currentScene.input?.toggleEnabled(!transition.blockInput); + this._engine.input?.toggleEnabled(!transition.blockInput); + this._engine.add(this.currentTransition); await this.currentTransition.done; + + currentScene.input?.toggleEnabled(sceneInputEnabled); } this.currentTransition?.kill(); this.currentTransition?.reset(); diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index 44d350073..f09737c12 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -352,6 +352,9 @@ export class Engine implements CanInitialize, */ public clock: Clock; + public readonly pointerScope: PointerScope; + public readonly grabWindowFocus: boolean; + /** * The width of the game canvas in pixels (physical width component of the * resolution of the canvas element) @@ -786,6 +789,9 @@ O|===|* >________________>\n\ this.backgroundColor = options.backgroundColor.clone(); } + this.grabWindowFocus = options.grabWindowFocus; + this.pointerScope = options.pointerScope; + this.maxFps = options.maxFps ?? this.maxFps; this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps; diff --git a/src/engine/Input/InputHost.ts b/src/engine/Input/InputHost.ts index 39f66ecf8..3d0ce6a15 100644 --- a/src/engine/Input/InputHost.ts +++ b/src/engine/Input/InputHost.ts @@ -34,6 +34,10 @@ export class InputHost { }); } + get enabled() { + return this._enabled; + } + toggleEnabled(enabled: boolean) { this._enabled = enabled; this.keyboard.toggleEnabled(this._enabled); diff --git a/src/engine/Input/PointerSystem.ts b/src/engine/Input/PointerSystem.ts index e4d732f5b..ce10f6979 100644 --- a/src/engine/Input/PointerSystem.ts +++ b/src/engine/Input/PointerSystem.ts @@ -28,7 +28,8 @@ export class PointerSystem extends System { public priority = SystemPriority.Higher; private _engine: Engine; - private _receiver: PointerEventReceiver; + private _receivers: PointerEventReceiver[]; + private _engineReceiver: PointerEventReceiver; query: Query; constructor(public world: World) { @@ -64,9 +65,11 @@ export class PointerSystem extends System { public lastFrameEntityToPointers = new Map(); public currentFrameEntityToPointers = new Map(); + private _scene: Scene; public initialize(world: World, scene: Scene): void { this._engine = scene.engine; + this._scene = scene; } private _sortedTransforms: TransformComponent[] = []; @@ -79,7 +82,8 @@ export class PointerSystem extends System { public preupdate(): void { // event receiver might change per frame - this._receiver = this._engine.input.pointers; + this._receivers = [this._engine.input.pointers, this._scene.input.pointers]; + this._engineReceiver = this._engine.input.pointers; if (this._zHasChanged) { this._sortedTransforms.sort((a, b) => { return b.z - a.z; @@ -128,11 +132,11 @@ export class PointerSystem extends System { this._dispatchEvents(this._sortedEntities); // Clear last frame's events - this._receiver.update(); + this._receivers.forEach(r => r.update()); this.lastFrameEntityToPointers.clear(); this.lastFrameEntityToPointers = new Map(this.currentFrameEntityToPointers); this.currentFrameEntityToPointers.clear(); - this._receiver.clear(); + this._receivers.forEach(r => r.clear()); } private _processPointerToEntity(entities: Entity[]) { @@ -140,6 +144,7 @@ export class PointerSystem extends System { let collider: ColliderComponent; let graphics: GraphicsComponent; let pointer: PointerComponent; + const receiver = this._engineReceiver; // TODO probably a spatial partition optimization here to quickly query bounds for pointer // doesn't seem to cause issues tho for perf @@ -154,7 +159,7 @@ export class PointerSystem extends System { collider.update(); const geom = collider.get(); if (geom) { - for (const [pointerId, pos] of this._receiver.currentFramePointerCoords.entries()) { + for (const [pointerId, pos] of receiver.currentFramePointerCoords.entries()) { if (geom.contains(transform.coordPlane === CoordPlane.World ? pos.worldPos : pos.screenPos)) { this.addPointerToEntity(entity, pointerId); } @@ -166,7 +171,7 @@ export class PointerSystem extends System { graphics = entity.get(GraphicsComponent); if (graphics && (pointer.useGraphicsBounds || this.overrideUseGraphicsBounds)) { const graphicBounds = graphics.localBounds.transform(transform.get().matrix); - for (const [pointerId, pos] of this._receiver.currentFramePointerCoords.entries()) { + for (const [pointerId, pos] of receiver.currentFramePointerCoords.entries()) { if (graphicBounds.contains(transform.coordPlane === CoordPlane.World ? pos.worldPos : pos.screenPos)) { this.addPointerToEntity(entity, pointerId); } @@ -176,12 +181,13 @@ export class PointerSystem extends System { } private _processDownAndEmit(entity: Entity): Map { - const lastDownPerPointer = new Map(); + const receiver = this._engineReceiver; + const lastDownPerPointer = new Map(); // TODO will this get confused between receivers? // Loop through down and dispatch to entities - for (const event of this._receiver.currentFrameDown) { + for (const event of receiver.currentFrameDown) { if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)) { entity.events.emit('pointerdown', event as any); - if (this._receiver.isDragStart(event.pointerId)) { + if (receiver.isDragStart(event.pointerId)) { entity.events.emit('pointerdragstart', event as any); } } @@ -191,12 +197,13 @@ export class PointerSystem extends System { } private _processUpAndEmit(entity: Entity): Map { + const receiver = this._engineReceiver; const lastUpPerPointer = new Map(); // Loop through up and dispatch to entities - for (const event of this._receiver.currentFrameUp) { + for (const event of receiver.currentFrameUp) { if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)) { entity.events.emit('pointerup', event as any); - if (this._receiver.isDragEnd(event.pointerId)) { + if (receiver.isDragEnd(event.pointerId)) { entity.events.emit('pointerdragend', event as any); } } @@ -206,14 +213,15 @@ export class PointerSystem extends System { } private _processMoveAndEmit(entity: Entity): Map { + const receiver = this._engineReceiver; const lastMovePerPointer = new Map(); // Loop through move and dispatch to entities - for (const event of this._receiver.currentFrameMove) { + for (const event of receiver.currentFrameMove) { if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)) { // move entity.events.emit('pointermove', event as any); - if (this._receiver.isDragging(event.pointerId)) { + if (receiver.isDragging(event.pointerId)) { entity.events.emit('pointerdragmove', event as any); } } @@ -223,12 +231,13 @@ export class PointerSystem extends System { } private _processEnterLeaveAndEmit(entity: Entity, lastUpDownMoveEvents: PointerEvent[]) { + const receiver = this._engineReceiver; // up, down, and move are considered for enter and leave for (const event of lastUpDownMoveEvents) { // enter if (event.active && entity.active && this.entered(entity, event.pointerId)) { entity.events.emit('pointerenter', event as any); - if (this._receiver.isDragging(event.pointerId)) { + if (receiver.isDragging(event.pointerId)) { entity.events.emit('pointerdragenter', event as any); } break; @@ -239,7 +248,7 @@ export class PointerSystem extends System { // or leave can happen on pointer up (this.entityCurrentlyUnderPointer(entity, event.pointerId) && event.type === 'up'))) { entity.events.emit('pointerleave', event as any); - if (this._receiver.isDragging(event.pointerId)) { + if (receiver.isDragging(event.pointerId)) { entity.events.emit('pointerdragleave', event as any); } break; @@ -248,8 +257,9 @@ export class PointerSystem extends System { } private _processCancelAndEmit(entity: Entity) { + const receiver = this._engineReceiver; // cancel - for (const event of this._receiver.currentFrameCancel) { + for (const event of receiver.currentFrameCancel) { if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)){ entity.events.emit('pointercancel', event as any); } @@ -257,8 +267,9 @@ export class PointerSystem extends System { } private _processWheelAndEmit(entity: Entity) { + const receiver = this._engineReceiver; // wheel - for (const event of this._receiver.currentFrameWheel) { + for (const event of receiver.currentFrameWheel) { // Currently the wheel only fires under the primary pointer '0' if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, 0)) { entity.events.emit('pointerwheel', event as any); diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts index 05cf69c90..06cbd968c 100644 --- a/src/engine/Scene.ts +++ b/src/engine/Scene.ts @@ -36,6 +36,8 @@ import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; import { Color } from './Color'; import { DefaultLoader } from './Director/DefaultLoader'; import { Transition } from './Director'; +import { InputHost } from './Input/InputHost'; +import { PointerScope } from './Input/PointerScope'; export class PreLoadEvent { loader: DefaultLoader; @@ -149,6 +151,11 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate */ public engine: Engine; + /** + * Access scene specific input, handlers on this only fire when this scene is active. + */ + public input: InputHost; + private _isInitialized: boolean = false; private _timers: Timer[] = []; public get timers(): readonly Timer[] { @@ -312,6 +319,11 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate public async _initialize(engine: Engine) { if (!this.isInitialized) { this.engine = engine; + this.input = new InputHost({ + pointerTarget: engine.pointerScope === PointerScope.Canvas ? engine.canvas : document, + grabWindowFocus: engine.grabWindowFocus, + engine + }); // Initialize camera first this.camera._initialize(engine); @@ -336,6 +348,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate */ public async _activate(context: SceneActivationContext) { this._logger.debug('Scene.onActivate', this); + this.input.toggleEnabled(true); await this.onActivate(context); } @@ -347,6 +360,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate */ public async _deactivate(context: SceneActivationContext) { this._logger.debug('Scene.onDeactivate', this); + this.input.toggleEnabled(false); await this.onDeactivate(context); } @@ -428,6 +442,8 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate this._collectActorStats(engine); this._postupdate(engine, delta); + + this.input.update(); } /** diff --git a/src/spec/DirectorSpec.ts b/src/spec/DirectorSpec.ts index 29abb9f87..8b49a1ad4 100644 --- a/src/spec/DirectorSpec.ts +++ b/src/spec/DirectorSpec.ts @@ -98,12 +98,18 @@ describe('A Director', () => { const clock = engine.clock as ex.TestClock; clock.start(); const scene1 = new ex.Scene(); + scene1._initialize(engine); const scene2 = new ex.Scene(); + scene2._initialize(engine); const sut = new ex.Director(engine, { scene1, scene2 }); + sut.rootScene._initialize(engine); + engine.rootScene._initialize(engine); const fadeIn = new ex.FadeInOut({ direction: 'in', duration: 1000}); + engine.screen.setCurrentCamera(engine.currentScene.camera); + fadeIn._initialize(engine); const loader = new ex.DefaultLoader(); sut.configureStart('scene1', { inTransition: fadeIn,