From 52c5c0d32e4f1f7811ccb7b67abc2f15addc4ab5 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Wed, 14 Feb 2024 20:13:52 -0600 Subject: [PATCH] fix: Scene transition bugs + Coroutines (#2932) This PR fixes some Scene transition bugs * If the root scene was overridden it would confuse the initial transition * goto and goToScene now behave the same with regards to scene transitions * Adds friendly warning instead of error when invalid scene update/draw's occur * Switches Transitions to work via coroutine removing some update code in director --- CHANGELOG.md | 36 +++++- sandbox/src/game.ts | 2 +- sandbox/tests/router/index.ts | 7 +- site/docs/02-fundamentals/05-transitions.mdx | 4 +- site/docs/02-fundamentals/06-loaders.mdx | 2 +- src/engine/Director/Director.ts | 31 ++--- src/engine/Director/Transition.ts | 24 +++- src/engine/Engine.ts | 47 +++++-- src/engine/EntityComponentSystem/Entity.ts | 2 +- src/engine/Scene.ts | 67 ++++++---- src/engine/Util/Clock.ts | 6 +- src/engine/Util/Coroutine.ts | 43 +++++++ src/engine/index.ts | 1 + src/spec/CoroutineSpec.ts | 82 ++++++++++++ src/spec/DirectorSpec.ts | 6 - src/spec/EngineSpec.ts | 8 +- src/spec/FadeInOutSpec.ts | 125 ++++++++----------- src/spec/SceneSpec.ts | 10 +- src/spec/TransistionSpec.ts | 16 +-- src/spec/util/TestUtils.ts | 10 ++ 20 files changed, 364 insertions(+), 165 deletions(-) create mode 100644 src/engine/Util/Coroutine.ts create mode 100644 src/spec/CoroutineSpec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index acf11edf4..4cd125801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,28 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes +- `ex.Engine.goToScene`'s second argument now takes `GoToOptions` instead of just scene activation data + ```typescript + { + /** + * Optionally supply scene activation data passed to Scene.onActivate + */ + sceneActivationData?: TActivationData, + /** + * Optionally supply destination scene "in" transition, this will override any previously defined transition + */ + destinationIn?: Transition, + /** + * Optionally supply source scene "out" transition, this will override any previously defined transition + */ + sourceOut?: Transition, + /** + * Optionally supply a different loader for the destination scene, this will override any previously defined loader + */ + loader?: DefaultLoader + } + ``` + - `ex.Physics` static is marked as deprecated, configuring these setting will move to the `ex.Engine({...})` constructor ```typescript const engine = new ex.Engine({ @@ -40,6 +62,18 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Experimental `ex.coroutine` for running code that changes over time, useful for modeling complex animation code. Coroutines return a promise when they are complete. You can think of each `yield` as a frame. + * The result of a yield is the current elapsed time + * You can yield a number in milliseconds and it will wait that long before resuming + * You can yield a promise and it will wait until it resolves before resuming + ```typescript + const completePromise = coroutine(engine, function * () { + let elapsed = 0; + elapsed = yield 200; // frame 1 wait 200 ms before resuming + elapsed = yield fetch('./some/data.json'); // frame 2 + elapsed = yield; // frame 3 + }); + ``` - Added additional options in rayCast options * `ignoreCollisionGroupAll: boolean` will ignore testing against anything with the `CollisionGroup.All` which is the default for all * `filter: (hit: RayCastHit) => boolean` will allow people to do arbitrary filtering on raycast results, this runs very last after all other collision group/collision mask decisions have been made @@ -153,7 +187,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). * New scene lifecycle to allow scene specific resource loading * `onTransition(direction: "in" | "out") {...}` * `onPreLoad(loader: DefaultLoader) {...}` - * New async goto API that allows overriding loaders/transitions between scenes + * New async `goToScene()` API that allows overriding loaders/transitions between scenes * Scenes now can have `async onInitialize` and `async onActivate`! * New scenes director API that allows upfront definition of scenes/transitions/loaders diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index c082f0e1c..e38165173 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -51,7 +51,7 @@ var game = new ex.Engine({ // pixelRatio: 1, // suppressPlayButton: true, pointerScope: ex.PointerScope.Canvas, - displayMode: ex.DisplayMode.FitScreenAndFill, + displayMode: ex.DisplayMode.FitScreenAndZoom, snapToPixel: false, fixedUpdateFps: 60, maxFps: 60, diff --git a/sandbox/tests/router/index.ts b/sandbox/tests/router/index.ts index a326a4723..f750ae228 100644 --- a/sandbox/tests/router/index.ts +++ b/sandbox/tests/router/index.ts @@ -102,10 +102,6 @@ scene2.onPreLoad = (loader) => { scene1.onActivate = () => { setTimeout(() => { gameWithTransitions.goto('scene2'); - // router.goto('scene2', { - // outTransition: new ex.FadeOut({duration: 1000, direction: 'in'}), - // inTransition: new ex.FadeOut({duration: 1000, direction: 'out'}) - // }); }, 1000); } scene2.add(new ex.Actor({ @@ -141,6 +137,5 @@ var startTransition = new ex.FadeInOut({duration: 500, direction: 'in', color: e // }) gameWithTransitions.start('scene1', { - inTransition: startTransition, - loader: boot + inTransition: startTransition }); \ No newline at end of file diff --git a/site/docs/02-fundamentals/05-transitions.mdx b/site/docs/02-fundamentals/05-transitions.mdx index 51a7d29cc..e5ee34194 100644 --- a/site/docs/02-fundamentals/05-transitions.mdx +++ b/site/docs/02-fundamentals/05-transitions.mdx @@ -55,7 +55,7 @@ const game = new ex.Engine({ }); -game.goto('scene1'); +game.goToScene('scene1'); ``` @@ -129,7 +129,7 @@ const transition = new ex.Transition({ There are 2 ways to override pre-defined transitions -* `goto('myscene', { destinationIn: ..., sourceOut: ... })` takes the highest precedence and will override any transition +* `goToScene('myscene', { destinationIn: ..., sourceOut: ... })` takes the highest precedence and will override any transition * Extending [[Scene.onTransition]] you can provide dynamic transitions depending on your scene's state ```typescript diff --git a/site/docs/02-fundamentals/06-loaders.mdx b/site/docs/02-fundamentals/06-loaders.mdx index 58236defb..d071b3880 100644 --- a/site/docs/02-fundamentals/06-loaders.mdx +++ b/site/docs/02-fundamentals/06-loaders.mdx @@ -126,7 +126,7 @@ const game = new ex.Engine({ } }); -game.goto('scene1'); +game.goToScene('scene1'); ``` diff --git a/src/engine/Director/Director.ts b/src/engine/Director/Director.ts index 70e8ecc50..0392bf4d5 100644 --- a/src/engine/Director/Director.ts +++ b/src/engine/Director/Director.ts @@ -71,11 +71,11 @@ export interface StartOptions { /** * Provide scene activation data and override any existing configured route transitions or loaders */ -export interface GoToOptions { +export interface GoToOptions { /** * Optionally supply scene activation data passed to Scene.onActivate */ - sceneActivationData?: any, + sceneActivationData?: TActivationData, /** * Optionally supply destination scene "in" transition, this will override any previously defined transition */ @@ -131,7 +131,7 @@ export class Director { mainLoader: DefaultLoader; /** - * The default [[Scene]] of the game, use [[Engine.goto]] to transition to different scenes. + * The default [[Scene]] of the game, use [[Engine.goToScene]] to transition to different scenes. */ public readonly rootScene: Scene; @@ -161,6 +161,10 @@ export class Director { for (const sceneKey in scenes) { const sceneOrOptions = scenes[sceneKey]; this.add(sceneKey, sceneOrOptions); + if (sceneKey === 'root') { + this.rootScene = this.getSceneInstance('root'); + this.currentScene = this.rootScene; + } } } @@ -266,6 +270,15 @@ export class Director { return undefined; } + getSceneName(scene: Scene) { + for (const [name, sceneInstance] of this._sceneToInstance) { + if (scene === sceneInstance) { + return name; + } + } + return 'unknown scene name'; + } + /** * Returns the same Director, but asserts a scene DOES exist to the type system * @param name @@ -480,8 +493,7 @@ export class Director { currentScene.input?.toggleEnabled(!transition.blockInput); this._engine.input?.toggleEnabled(!transition.blockInput); - this._engine.add(this.currentTransition); - await this.currentTransition.done; + await this.currentTransition.play(this._engine); currentScene.input?.toggleEnabled(sceneInputEnabled); } @@ -547,15 +559,6 @@ export class Director { destinationName: destinationScene } as DirectorNavigationEvent); } - - /** - * Updates internal transitions - */ - update() { - if (this.currentTransition) { - this.currentTransition.execute(); - } - } } diff --git a/src/engine/Director/Transition.ts b/src/engine/Director/Transition.ts index 103295c03..6e6288760 100644 --- a/src/engine/Director/Transition.ts +++ b/src/engine/Director/Transition.ts @@ -7,6 +7,8 @@ import { CoordPlane } from '../Math/coord-plane'; import { Vector } from '../Math/vector'; import { clamp } from '../Math/util'; import { EasingFunction, EasingFunctions } from '../Util/EasingFunctions'; +import { coroutine } from '../Util/Coroutine'; +import { Logger } from '../Util/Log'; export interface TransitionOptions { /** @@ -47,6 +49,7 @@ export interface TransitionOptions { * Base Transition that can be extended to provide custom scene transitions in Excalibur. */ export class Transition extends Entity { + private _logger: Logger = Logger.getInstance(); transform = new TransformComponent(); graphics = new GraphicsComponent(); readonly hideLoader: boolean; @@ -106,11 +109,11 @@ export class Transition extends Entity { /** * Overridable lifecycle method, called before each update. * - * **WARNING BE SURE** to call `super.onPreUpdate()` if overriding in your own custom implementation + * **WARNING BE SURE** to call `super.updateTransition()` if overriding in your own custom implementation * @param engine * @param delta */ - public override onPreUpdate(engine: Engine, delta: number): void { + public updateTransition(engine: Engine, delta: number): void { if (this.complete) { return; } @@ -192,6 +195,23 @@ export class Transition extends Entity { this.onReset(); } + play(engine: Engine) { + if (this.started) { + this._logger.warn(`Attempted to play a transition ${this.name} that is already playing`); + return Promise.resolve(); + } + + engine.add(this); + const self = this; + return coroutine(engine, function * () { + while (!self.complete) { + const elapsed = yield; // per frame + self.updateTransition(engine, elapsed); + self.execute(); + } + }); + } + /** * execute() is called by the engine every frame to update the Transition lifecycle onStart/onUpdate/onEnd */ diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index e6d7345f7..fe6ac79c5 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -1,5 +1,4 @@ import { EX_VERSION } from './'; -import { obsolete } from './Util/Decorators'; import { Future } from './Util/Future'; import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; import { PointerScope } from './Input/PointerScope'; @@ -332,7 +331,7 @@ export interface EngineOptions { /** * Optionally specify scenes with their transitions and loaders to excalibur's scene [[Director]] * - * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goto` + * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goToScene` */ scenes?: SceneMap } @@ -531,7 +530,7 @@ export class Engine implements CanInitialize, } /** - * The default [[Scene]] of the game, use [[Engine.goto]] to transition to different scenes. + * The default [[Scene]] of the game, use [[Engine.goToScene]] to transition to different scenes. */ public get rootScene(): Scene { return this.director.rootScene; @@ -1230,7 +1229,7 @@ O|===|* >________________>\n\ * * Example: * ```typescript - * game.goto('myScene', { + * game.goToScene('myScene', { * sceneActivationData: {any: 'thing at all'}, * destinationIn: new FadeInOut({duration: 1000, direction: 'in'}), * sourceOut: new FadeInOut({duration: 1000, direction: 'out'}), @@ -1251,21 +1250,44 @@ O|===|* >________________>\n\ * ``` * @param destinationScene * @param options + * @deprecated use goToScene, it now behaves the same as goto */ public async goto(destinationScene: WithRoot, options?: GoToOptions) { await this.director.goto(destinationScene, options); } /** - * Changes the currently updating and drawing scene to a different, - * named scene. Calls the [[Scene]] lifecycle events. - * @param key The key of the scene to transition to. - * @param data Optional data to send to the scene's onActivate method - * @deprecated Use [[Engine.goto]] will be removed in v1! + * Changes the current scene with optionally supplied: + * * Activation data + * * Transitions + * * Loaders + * + * Example: + * ```typescript + * game.goToScene('myScene', { + * sceneActivationData: {any: 'thing at all'}, + * destinationIn: new FadeInOut({duration: 1000, direction: 'in'}), + * sourceOut: new FadeInOut({duration: 1000, direction: 'out'}), + * loader: MyLoader + * }); + * ``` + * + * Scenes are defined in the Engine constructor + * ```typescript + * const engine = new ex.Engine({ + scenes: {...} + }); + * ``` + * Or by adding dynamically + * + * ```typescript + * engine.addScene('myScene', new ex.Scene()); + * ``` + * @param destinationScene + * @param options */ - @obsolete({message: 'Engine.goToScene is deprecated, will be removed in v1', alternateMethod: 'Engine.goto'}) - public async goToScene(key: string, data?: TData): Promise { - await this.director.swapScene(key, data); + public async goToScene(destinationScene: WithRoot, options?: GoToOptions): Promise { + await this.director.goto(destinationScene, options); } /** @@ -1367,7 +1389,6 @@ O|===|* >________________>\n\ * @param delta Number of milliseconds elapsed since the last update. */ private _update(delta: number) { - this.director.update(); if (this._isLoading) { // suspend updates until loading is finished this._loader?.onUpdate(this, delta); diff --git a/src/engine/EntityComponentSystem/Entity.ts b/src/engine/EntityComponentSystem/Entity.ts index 7db1aa630..1acfc1326 100644 --- a/src/engine/EntityComponentSystem/Entity.ts +++ b/src/engine/EntityComponentSystem/Entity.ts @@ -562,7 +562,7 @@ export class Entity implements OnIniti /** * - *Entity update lifecycle, called internally + * Entity update lifecycle, called internally * @internal * @param engine * @param delta diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts index 2b6d33e18..8d34db157 100644 --- a/src/engine/Scene.ts +++ b/src/engine/Scene.ts @@ -217,11 +217,11 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate /** * Event hook fired directly before transition, either "in" or "out" of the scene * - * This overrides the Engine scene definition. However transitions specified in goto take hightest precedence + * This overrides the Engine scene definition. However transitions specified in goToScene take highest precedence * * ```typescript * // Overrides all - * Engine.goto('scene', { destinationIn: ..., sourceOut: ... }); + * Engine.goToScene('scene', { destinationIn: ..., sourceOut: ... }); * ``` * * This can be used to configure custom transitions for a scene dynamically @@ -319,26 +319,31 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate */ public async _initialize(engine: Engine) { if (!this.isInitialized) { - this.engine = engine; - // PhysicsWorld config is watched so things will automagically update - this.physics.config = this.engine.physics; - this.input = new InputHost({ - pointerTarget: engine.pointerScope === PointerScope.Canvas ? engine.canvas : document, - grabWindowFocus: engine.grabWindowFocus, - engine - }); - // Initialize camera first - this.camera._initialize(engine); - - this.world.systemManager.initialize(); - - // This order is important! we want to be sure any custom init that add actors - // fire before the actor init - await this.onInitialize(engine); - this._initializeChildren(); - - this._logger.debug('Scene.onInitialize', this, engine); - this.events.emit('initialize', new InitializeEvent(engine, this)); + try { + this.engine = engine; + // PhysicsWorld config is watched so things will automagically update + this.physics.config = this.engine.physics; + this.input = new InputHost({ + pointerTarget: engine.pointerScope === PointerScope.Canvas ? engine.canvas : document, + grabWindowFocus: engine.grabWindowFocus, + engine + }); + // Initialize camera first + this.camera._initialize(engine); + + this.world.systemManager.initialize(); + + // This order is important! we want to be sure any custom init that add actors + // fire before the actor init + await this.onInitialize(engine); + this._initializeChildren(); + + this._logger.debug('Scene.onInitialize', this, engine); + this.events.emit('initialize', new InitializeEvent(engine, this)); + } catch (e) { + this._logger.error(`Error during scene initialization for scene ${engine.director?.getSceneName(this)}!`); + throw e; + } this._isInitialized = true; } } @@ -350,9 +355,14 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate * @internal */ public async _activate(context: SceneActivationContext) { - this._logger.debug('Scene.onActivate', this); - this.input.toggleEnabled(true); - await this.onActivate(context); + try { + this._logger.debug('Scene.onActivate', this); + this.input.toggleEnabled(true); + await this.onActivate(context); + } catch (e) { + this._logger.error(`Error during scene activation for scene ${this.engine?.director?.getSceneName(this)}!`); + throw e; + } } /** @@ -418,7 +428,8 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate */ public update(engine: Engine, delta: number) { if (!this.isInitialized) { - throw new Error('Scene update called before it was initialized! Was there an error in actor or entity initialization?'); + this._logger.warnOnce(`Scene update called before initialize for scene ${engine.director?.getSceneName(this)}!`); + return; } this._preupdate(engine, delta); @@ -455,6 +466,10 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate * @param delta The number of milliseconds since the last draw */ public draw(ctx: ExcaliburGraphicsContext, delta: number) { + if (!this.isInitialized) { + this._logger.warnOnce(`Scene draw called before initialize!`); + return; + } this._predraw(ctx, delta); this.world.update(SystemType.Draw, delta); diff --git a/src/engine/Util/Clock.ts b/src/engine/Util/Clock.ts index 1076680e1..b090504f3 100644 --- a/src/engine/Util/Clock.ts +++ b/src/engine/Util/Clock.ts @@ -35,7 +35,7 @@ export abstract class Clock { public fpsSampler: FpsSampler; private _options: ClockOptions; private _elapsed: number = 1; - private _scheduledCbs: [cb: () => any, scheduledTime: number][] = []; + private _scheduledCbs: [cb: (elapsedMs: number) => any, scheduledTime: number][] = []; private _totalElapsed: number = 0; constructor(options: ClockOptions) { this._options = options; @@ -91,7 +91,7 @@ export abstract class Clock { * @param cb callback to fire * @param timeoutMs Optionally specify a timeout in milliseconds from now, default is 0ms which means the next possible tick */ - public schedule(cb: () => any, timeoutMs: number = 0) { + public schedule(cb: (elapsedMs: number) => any, timeoutMs: number = 0) { // Scheduled based on internal elapsed time const scheduledTime = this._totalElapsed + timeoutMs; this._scheduledCbs.push([cb, scheduledTime]); @@ -101,7 +101,7 @@ export abstract class Clock { // walk backwards to delete items as we loop for (let i = this._scheduledCbs.length - 1; i > -1; i--) { if (this._scheduledCbs[i][1] <= this._totalElapsed) { - this._scheduledCbs[i][0](); + this._scheduledCbs[i][0](this._elapsed); this._scheduledCbs.splice(i, 1); } } diff --git a/src/engine/Util/Coroutine.ts b/src/engine/Util/Coroutine.ts new file mode 100644 index 000000000..ec37d3270 --- /dev/null +++ b/src/engine/Util/Coroutine.ts @@ -0,0 +1,43 @@ +import { Engine } from '../Engine'; +export type CoroutineGenerator = () => Generator, void, number>; + +/** + * Excalibur coroutine helper, returns a promise when complete. Coroutines run before frame update. + * + * Each coroutine yield is 1 excalibur frame. Coroutines get passed the elapsed time our of yield. Coroutines + * run internally on the excalibur clock. + * + * If you yield a promise it will be awaited before resumed + * If you yield a number it will wait that many ms before resumed + * @param engine + * @param coroutineGenerator + */ +export function coroutine(engine: Engine, coroutineGenerator: CoroutineGenerator): Promise { + return new Promise((resolve, reject) => { + const generator = coroutineGenerator(); + const loop = (elapsedMs: number) => { + try { + const { done, value } = generator.next(elapsedMs); + if (done) { + resolve(); + } + + if (value instanceof Promise) { + value.then(() => { + // schedule next loop + engine.clock.schedule(loop); + }); + } else if (value === undefined || value === (void 0)) { + // schedule next frame + engine.clock.schedule(loop); + } else { + // schedule value milliseconds from now + engine.clock.schedule(loop, value || 0); + } + } catch (e) { + reject(e); + } + }; + loop(engine.clock.elapsed());// run first frame immediately + }); +} \ No newline at end of file diff --git a/src/engine/index.ts b/src/engine/index.ts index 7b8f39b49..2301615e4 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -135,6 +135,7 @@ export * from './Util/Toaster'; export * from './Util/StateMachine'; export * from './Util/Future'; export * from './Util/Semaphore'; +export * from './Util/Coroutine'; // ex.Deprecated // import * as deprecated from './Deprecated'; diff --git a/src/spec/CoroutineSpec.ts b/src/spec/CoroutineSpec.ts new file mode 100644 index 000000000..c5225b63b --- /dev/null +++ b/src/spec/CoroutineSpec.ts @@ -0,0 +1,82 @@ +import * as ex from '@excalibur'; +import { TestUtils } from './util/TestUtils'; + +describe('A Coroutine', () => { + it('exists', () => { + expect(ex.coroutine).toBeDefined(); + }); + + it('can be run', async () => { + const engine = TestUtils.engine({ width: 100, height: 100}); + const clock = engine.clock as ex.TestClock; + clock.start(); + const result = ex.coroutine(engine, function * () { + const elapsed = yield; + expect(elapsed).toBe(100); + yield; + }); + clock.step(100); + clock.step(100); + await expectAsync(result).toBeResolved(); + }); + + it('can wait for given ms', async () => { + const engine = TestUtils.engine({ width: 100, height: 100}); + const clock = engine.clock as ex.TestClock; + clock.start(); + const result = ex.coroutine(engine, function * () { + const elapsed = yield 200; + expect(elapsed).toBe(200); + yield; + + }); + // wait 200 ms + clock.step(200); + // 1 more yield + clock.step(100); + await expectAsync(result).toBeResolved(); + }); + + it('can wait for a promise', async () => { + const engine = TestUtils.engine({ width: 100, height: 100}); + const clock = engine.clock as ex.TestClock; + clock.start(); + const result = ex.coroutine(engine, function * () { + const elapsed = yield ex.Util.delay(1000, clock); + expect(elapsed).toBe(1); + yield; + }); + // wait 200 ms + clock.step(1000); + + // flush + await Promise.resolve(); + clock.step(0); + + // 1 more yield + clock.step(100); + await expectAsync(result).toBeResolved(); + }); + + it('can throw error', async () => { + const engine = TestUtils.engine({ width: 100, height: 100}); + const clock = engine.clock as ex.TestClock; + clock.start(); + const result = ex.coroutine(engine, function * () { + const elapsed = yield ex.Util.delay(1000, clock); + expect(elapsed).toBe(1); + yield; + throw Error('error'); + }); + // wait 200 ms + clock.step(1000); + + // flush + await Promise.resolve(); + clock.step(0); + + // 1 more yield + clock.step(100); + await expectAsync(result).toBeRejectedWithError('error'); + }); +}); \ No newline at end of file diff --git a/src/spec/DirectorSpec.ts b/src/spec/DirectorSpec.ts index e1976c2ff..706c99ee8 100644 --- a/src/spec/DirectorSpec.ts +++ b/src/spec/DirectorSpec.ts @@ -62,7 +62,6 @@ describe('A Director', () => { loader }); await engine.load(loader); - sut.update(); expect(sut.currentTransition).toBe(fadeIn); expect(sut.currentSceneName).toBe('scene1'); @@ -86,7 +85,6 @@ describe('A Director', () => { sut.onInitialize(); await engine.load(loader); - sut.update(); expect(sut.currentTransition).toBe(fadeIn); expect(sut.currentSceneName).toBe('scene1'); @@ -121,13 +119,9 @@ describe('A Director', () => { await (engine as any)._overrideInitialize(engine); clock.step(100); - sut.update(); clock.step(100); - sut.update(); clock.step(100); - sut.update(); clock.step(100); - sut.update(); expect(sut.currentTransition).toBe(fadeIn); expect(sut.currentSceneName).toBe('scene1'); diff --git a/src/spec/EngineSpec.ts b/src/spec/EngineSpec.ts index 85acfecb3..1a9aeb20d 100644 --- a/src/spec/EngineSpec.ts +++ b/src/spec/EngineSpec.ts @@ -638,10 +638,10 @@ describe('The engine', () => { expect(engine.currentScene.actors.length).toBe(0); }); - it('will log an error if the scene does not exist', () => { - spyOn(ex.Logger.getInstance(), 'error'); - engine.goToScene('madeUp'); - expect(ex.Logger.getInstance().error).toHaveBeenCalledWith('Scene', 'madeUp', 'does not exist!'); + it('will log an error if the scene does not exist', async () => { + spyOn(ex.Logger.getInstance(), 'warn'); + await engine.goToScene('madeUp'); + expect(ex.Logger.getInstance().warn).toHaveBeenCalledWith('Scene madeUp does not exist! Check the name, are you sure you added it?'); }); it('will add actors to the correct scene when initialized after a deferred goTo', async () => { diff --git a/src/spec/FadeInOutSpec.ts b/src/spec/FadeInOutSpec.ts index b0fca7d88..f30856210 100644 --- a/src/spec/FadeInOutSpec.ts +++ b/src/spec/FadeInOutSpec.ts @@ -11,93 +11,74 @@ describe('A FadeInOut transition', () => { }); it('can be constructed', () => { - const sut = new ex.FadeInOut({duration: 1000, color: ex.Color.Red}); + const sut = new ex.FadeInOut({ duration: 1000, color: ex.Color.Red }); expect(sut.duration).toBe(1000); expect(sut.name).toContain('FadeInOut#'); expect(sut.color).toEqual(ex.Color.Red); }); - it('can fade in', (done) => { - const engine = TestUtils.engine({backgroundColor: ex.Color.ExcaliburBlue}); + it('can fade in', async () => { + const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); const clock = engine.clock as ex.TestClock; - TestUtils.runToReady(engine).then(() => { - engine.add(new ex.Actor({ - pos: ex.vec(20, 20), - width: 100, - height: 100, - color: ex.Color.Red - })); + await TestUtils.runToReady(engine); + engine.add(new ex.Actor({ + pos: ex.vec(20, 20), + width: 100, + height: 100, + color: ex.Color.Red + })); - const onDeactivateSpy = jasmine.createSpy('onDeactivate').and.callFake(async () => { - await Promise.resolve(); - }); + const onDeactivateSpy = jasmine.createSpy('onDeactivate').and.callFake(async () => { + await Promise.resolve(); + }); - engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; + engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; - const sut = new ex.FadeInOut({duration: 1000, direction: 'in'}); - const scene = new ex.Scene(); - scene.add(new ex.Actor({ - pos: ex.vec(200, 200), - width: 40, - height: 40, - color: ex.Color.Violet - })); - engine.addScene('newScene', { scene, transitions: {in: sut}}); + const sut = new ex.FadeInOut({ duration: 1000, direction: 'in' }); + const scene = new ex.Scene(); + scene.add(new ex.Actor({ + pos: ex.vec(200, 200), + width: 40, + height: 40, + color: ex.Color.Violet + })); + engine.addScene('newScene', { scene, transitions: { in: sut } }); - const goto = engine.goto('newScene'); - setTimeout(() => { - clock.step(1); - }); - setTimeout(() => { - clock.step(500); - }); - setTimeout(() => { - clock.step(1); - expect(onDeactivateSpy).toHaveBeenCalledTimes(1); - expectAsync(engine.canvas).toEqualImage('/src/spec/images/FadeInOutSpec/fadein.png').then(() => { - done(); - engine.dispose(); - }); - }); - }); + const goto = engine.goto('newScene'); + await TestUtils.flushMicrotasks(clock, 13); + clock.step(500); + await Promise.resolve(); + expect(onDeactivateSpy).toHaveBeenCalledTimes(1); + await expectAsync(engine.canvas).toEqualImage('/src/spec/images/FadeInOutSpec/fadein.png'); + engine.dispose(); }); - it('can fade out', (done) => { - const engine = TestUtils.engine({backgroundColor: ex.Color.ExcaliburBlue}); + it('can fade out', async () => { + const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); const clock = engine.clock as ex.TestClock; - TestUtils.runToReady(engine).then(() => { - engine.add(new ex.Actor({ - pos: ex.vec(20, 20), - width: 100, - height: 100, - color: ex.Color.Red - })); + TestUtils.runToReady(engine); + engine.add(new ex.Actor({ + pos: ex.vec(20, 20), + width: 100, + height: 100, + color: ex.Color.Red + })); - const sut = new ex.FadeInOut({duration: 1000, direction: 'out', color: ex.Color.Violet}); - const scene = new ex.Scene(); - scene.add(new ex.Actor({ - pos: ex.vec(200, 200), - width: 40, - height: 40, - color: ex.Color.Violet - })); - engine.addScene('newScene', scene); + const sut = new ex.FadeInOut({ duration: 1000, direction: 'out', color: ex.Color.Violet }); + const scene = new ex.Scene(); + scene.add(new ex.Actor({ + pos: ex.vec(200, 200), + width: 40, + height: 40, + color: ex.Color.Violet + })); + engine.addScene('newScene', scene); - const goto = engine.goto('newScene', {sourceOut: sut}); - setTimeout(() => { - clock.step(1); - }); - setTimeout(() => { - clock.step(900); - }); - setTimeout(() => { - clock.step(1); - expectAsync(engine.canvas).toEqualImage('/src/spec/images/FadeInOutSpec/fadeout.png').then(() => { - done(); - engine.dispose(); - }); - }); - }); + const goto = engine.goto('newScene', { sourceOut: sut }); + await TestUtils.flushMicrotasks(clock, 3); + clock.step(900); + await Promise.resolve(); + await expectAsync(engine.canvas).toEqualImage('/src/spec/images/FadeInOutSpec/fadeout.png'); }); }); \ No newline at end of file diff --git a/src/spec/SceneSpec.ts b/src/spec/SceneSpec.ts index e51f52a7f..8ca6ffb12 100644 --- a/src/spec/SceneSpec.ts +++ b/src/spec/SceneSpec.ts @@ -337,7 +337,7 @@ describe('A scene', () => { await engine.goToScene('sceneA'); - await engine.goToScene('sceneB', { foo: 'bar' }); + await engine.goToScene('sceneB', { sceneActivationData: { foo: 'bar' }}); expect(sceneA.onDeactivate).toHaveBeenCalledWith({ engine, @@ -820,8 +820,8 @@ describe('A scene', () => { expect(scene.onPreUpdate).toHaveBeenCalledTimes(2); }); - it('can have onPreDraw overridden safely', () => { - scene._initialize(engine); + it('can have onPreDraw overridden safely', async () => { + await scene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); scene.onPreDraw = (ctx, delta) => { expect(ctx).not.toBe(null); @@ -838,8 +838,8 @@ describe('A scene', () => { expect(scene.onPreDraw).toHaveBeenCalledTimes(2); }); - it('can have onPostDraw overridden safely', () => { - scene._initialize(engine); + it('can have onPostDraw overridden safely', async () => { + await scene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); scene.onPostDraw = (ctx, delta) => { expect(ctx).not.toBe(null); diff --git a/src/spec/TransistionSpec.ts b/src/spec/TransistionSpec.ts index 731893032..2250981ab 100644 --- a/src/spec/TransistionSpec.ts +++ b/src/spec/TransistionSpec.ts @@ -63,20 +63,20 @@ describe('A Transition', () => { expect(sut.onStart).toHaveBeenCalledWith(0); expect(sut.onUpdate).toHaveBeenCalledWith(0); - sut.onPreUpdate(engine, 16); + sut.updateTransition(engine, 16); sut.execute(); expect(onUpdateSpy.calls.argsFor(1)).toEqual([16/3000]); - sut.onPreUpdate(engine, 16); + sut.updateTransition(engine, 16); sut.execute(); expect(onUpdateSpy.calls.argsFor(2)).toEqual([32/3000]); - sut.onPreUpdate(engine, 3200 -32); + sut.updateTransition(engine, 3200 -32); sut.execute(); expect(onEndSpy).toHaveBeenCalledWith(1); expect(sut.complete).toBe(true); - sut.onPreUpdate(engine, 4000); + sut.updateTransition(engine, 4000); sut.execute(); // Start and end should only be called once @@ -102,20 +102,20 @@ describe('A Transition', () => { expect(sut.onStart).toHaveBeenCalledWith(0); expect(sut.onUpdate).toHaveBeenCalledWith(0); - sut.onPreUpdate(engine, 16); + sut.updateTransition(engine, 16); sut.execute(); expect(onUpdateSpy.calls.argsFor(1)).toEqual([16/3000]); - sut.onPreUpdate(engine, 16); + sut.updateTransition(engine, 16); sut.execute(); expect(onUpdateSpy.calls.argsFor(2)).toEqual([32/3000]); - sut.onPreUpdate(engine, 3200 -32); + sut.updateTransition(engine, 3200 -32); sut.execute(); expect(onEndSpy).toHaveBeenCalledWith(1); expect(sut.complete).toBe(true); - sut.onPreUpdate(engine, 4000); + sut.updateTransition(engine, 4000); sut.execute(); expect(sut.complete).toBe(true); diff --git a/src/spec/util/TestUtils.ts b/src/spec/util/TestUtils.ts index 084b2fa21..e9d2092fc 100644 --- a/src/spec/util/TestUtils.ts +++ b/src/spec/util/TestUtils.ts @@ -60,6 +60,16 @@ export namespace TestUtils { await start; } + /** + * + */ + export async function flushMicrotasks(clock: ex.TestClock, times: number) { + for ( let i = 0; i < times; i++) { + clock.step(0); + await Promise.resolve(); + } + } + /** * */