Skip to content

Commit

Permalink
feat: Scene input API (#2889)
Browse files Browse the repository at this point in the history
This PR enables scene specific input APIs that only fire handlers when the scene is active! This is very useful if you have a lot of handlers to manage and don't want to keep track of engine level subscriptions.

```typescript
class SceneWithInput extends ex.Scene {
  onInitialize(engine: ex.Engine<any>): void {
    this.input.pointers.on('down', () => {
      console.log('pointer down from scene1');
    });
  }
}
class OtherSceneWithInput extends ex.Scene {
  onInitialize(engine: ex.Engine<any>): void {
    this.input.pointers.on('down', () => {
      console.log('pointer down from scene2');
    });
  }
}
```

https://github.com/excaliburjs/Excalibur/assets/612071/b6759145-d45b-4212-92a6-d0a4f8ff990b
  • Loading branch information
eonarheim authored Jan 29, 2024
1 parent 93d039c commit e397cbb
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 21 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>): void {
this.input.pointers.on('down', () => {
console.log('pointer down from scene1');
});
}
}
class OtherSceneWithInput extends ex.Scene {
onInitialize(engine: ex.Engine<any>): void {
this.input.pointers.on('down', () => {
console.log('pointer down from scene2');
});
}
}
```

### Fixed

Expand Down
11 changes: 9 additions & 2 deletions sandbox/tests/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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');
Expand Down
12 changes: 12 additions & 0 deletions sandbox/tests/scene-input/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scene Input</title>
</head>
<body>
<script src="../../lib/excalibur.js"></script>
<script src="index.js"></script>
</body>
</html>
69 changes: 69 additions & 0 deletions sandbox/tests/scene-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/// <reference path='../../lib/excalibur.d.ts' />

const font = new ex.Font({
size: 48,
family: 'sans-serif',
baseAlign: ex.BaseAlign.Top
});

class SceneWithInput extends ex.Scene {
onInitialize(engine: ex.Engine<any>): 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<any>): 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');
12 changes: 10 additions & 2 deletions src/engine/Director/Director.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export class Director<TKnownScenes extends string = any> {
}

const sourceScene = this.currentSceneName;
const engineInputEnabled = this._engine.input?.enabled ?? true;
this._isTransitioning = true;

const maybeSourceOut = this.getSceneInstance(sourceScene)?.onTransition('out');
Expand Down Expand Up @@ -416,7 +417,7 @@ export class Director<TKnownScenes extends string = any> {
await this.playTransition(inTransition);
this._emitEvent('navigationend', sourceScene, destinationScene);

this._engine.toggleInputEnabled(true);
this._engine.input?.toggleEnabled(engineInputEnabled);
this._isTransitioning = false;
}

Expand Down Expand Up @@ -473,9 +474,16 @@ export class Director<TKnownScenes extends string = any> {
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();
Expand Down
6 changes: 6 additions & 0 deletions src/engine/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ export class Engine<TKnownScenes extends string = any> 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)
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions src/engine/Input/InputHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class InputHost {
});
}

get enabled() {
return this._enabled;
}

toggleEnabled(enabled: boolean) {
this._enabled = enabled;
this.keyboard.toggleEnabled(this._enabled);
Expand Down
45 changes: 28 additions & 17 deletions src/engine/Input/PointerSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof TransformComponent | typeof PointerComponent>;

constructor(public world: World) {
Expand Down Expand Up @@ -64,9 +65,11 @@ export class PointerSystem extends System {

public lastFrameEntityToPointers = new Map<number, number[]>();
public currentFrameEntityToPointers = new Map<number, number[]>();
private _scene: Scene<unknown>;

public initialize(world: World, scene: Scene): void {
this._engine = scene.engine;
this._scene = scene;
}

private _sortedTransforms: TransformComponent[] = [];
Expand All @@ -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;
Expand Down Expand Up @@ -128,18 +132,19 @@ 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<number, number[]>(this.currentFrameEntityToPointers);
this.currentFrameEntityToPointers.clear();
this._receiver.clear();
this._receivers.forEach(r => r.clear());
}

private _processPointerToEntity(entities: Entity[]) {
let transform: TransformComponent;
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
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -176,12 +181,13 @@ export class PointerSystem extends System {
}

private _processDownAndEmit(entity: Entity): Map<number, PointerEvent> {
const lastDownPerPointer = new Map<number, PointerEvent>();
const receiver = this._engineReceiver;
const lastDownPerPointer = new Map<number, PointerEvent>(); // 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);
}
}
Expand All @@ -191,12 +197,13 @@ export class PointerSystem extends System {
}

private _processUpAndEmit(entity: Entity): Map<number, PointerEvent> {
const receiver = this._engineReceiver;
const lastUpPerPointer = new Map<number, PointerEvent>();
// 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);
}
}
Expand All @@ -206,14 +213,15 @@ export class PointerSystem extends System {
}

private _processMoveAndEmit(entity: Entity): Map<number, PointerEvent> {
const receiver = this._engineReceiver;
const lastMovePerPointer = new Map<number, PointerEvent>();
// 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);
}
}
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -248,17 +257,19 @@ 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);
}
}
}

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);
Expand Down
Loading

0 comments on commit e397cbb

Please sign in to comment.