Skip to content

Commit

Permalink
fix: Scene transition bugs + Coroutines (#2932)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
eonarheim authored Feb 15, 2024
1 parent bfb72af commit 52c5c0d
Show file tree
Hide file tree
Showing 20 changed files with 364 additions and 165 deletions.
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion sandbox/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 1 addition & 6 deletions sandbox/tests/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -141,6 +137,5 @@ var startTransition = new ex.FadeInOut({duration: 500, direction: 'in', color: e
// })
gameWithTransitions.start('scene1',
{
inTransition: startTransition,
loader: boot
inTransition: startTransition
});
4 changes: 2 additions & 2 deletions site/docs/02-fundamentals/05-transitions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const game = new ex.Engine({
});


game.goto('scene1');
game.goToScene('scene1');

```

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion site/docs/02-fundamentals/06-loaders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const game = new ex.Engine({
}
});

game.goto('scene1');
game.goToScene('scene1');

```

Expand Down
31 changes: 17 additions & 14 deletions src/engine/Director/Director.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TActivationData = any> {
/**
* 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
*/
Expand Down Expand Up @@ -131,7 +131,7 @@ export class Director<TKnownScenes extends string = any> {
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;

Expand Down Expand Up @@ -161,6 +161,10 @@ export class Director<TKnownScenes extends string = any> {
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;
}
}
}

Expand Down Expand Up @@ -266,6 +270,15 @@ export class Director<TKnownScenes extends string = any> {
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
Expand Down Expand Up @@ -480,8 +493,7 @@ export class Director<TKnownScenes extends string = any> {
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);
}
Expand Down Expand Up @@ -547,15 +559,6 @@ export class Director<TKnownScenes extends string = any> {
destinationName: destinationScene
} as DirectorNavigationEvent);
}

/**
* Updates internal transitions
*/
update() {
if (this.currentTransition) {
this.currentTransition.execute();
}
}
}


24 changes: 22 additions & 2 deletions src/engine/Director/Transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
*/
Expand Down
47 changes: 34 additions & 13 deletions src/engine/Engine.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -332,7 +331,7 @@ export interface EngineOptions<TKnownScenes extends string = any> {
/**
* 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<TKnownScenes>
}
Expand Down Expand Up @@ -531,7 +530,7 @@ export class Engine<TKnownScenes extends string = any> 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;
Expand Down Expand Up @@ -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'}),
Expand All @@ -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<TKnownScenes>, 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<TData = undefined>(key: string, data?: TData): Promise<void> {
await this.director.swapScene(key, data);
public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> {
await this.director.goto(destinationScene, options);
}

/**
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/engine/EntityComponentSystem/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ export class Entity<TKnownComponents extends Component = any> implements OnIniti

/**
*
*Entity update lifecycle, called internally
* Entity update lifecycle, called internally
* @internal
* @param engine
* @param delta
Expand Down
Loading

0 comments on commit 52c5c0d

Please sign in to comment.