Skip to content

Commit

Permalink
feat: Excalibur Scene Transitions & Loaders (#2790)
Browse files Browse the repository at this point in the history
This PR  implements an API inspired by https://github.com/mattjennings/excalibur-router/ with permission from Matt.

The idea behind this refactoring is to add transitions between scenes and scene specific loaders!
* Add or remove scenes by constructor
* Add loaders by constructor
* New `DefaultLoader` type that allows for easier custom loader creation
* New `Transition` type for building custom transitions
* 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
* Scenes now can have `async onInitialize` and `async onActivate`!
* New scenes director API that allows upfront definition of scenes/transitions/loaders


View [this file](https://github.com/excaliburjs/Excalibur/blob/feat/async-init-ex-router/sandbox/tests/router/index.ts) for a current example of what the API looks like 


Defining scenes upfront
```typescript
const game = new ex.Engine({
  scenes: {
    scene1: {
      scene: scene1,
      transitions: {
        out: new ex.FadeInOut({duration: 1000, direction: 'out', color: ex.Color.Black}),
        in: new ex.FadeInOut({duration: 1000, direction: 'in'})
      }
    },
    scene2: {
      scene: scene2,
      loader: ex.DefaultLoader, // Constructor only option!
      transitions: {
        out: new ex.FadeInOut({duration: 1000, direction: 'out'}),
        in: new ex.FadeInOut({duration: 1000, direction: 'in', color: ex.Color.Black })
      }
    },
   scene3: ex.Scene // Constructor only option!
  } 
})

game.start('scene1',
{
  inTransition: new ex.FadeInOut({duration: 500, direction: 'in', color: ex.Color.ExcaliburBlue})
  loader: boot,
});
```

![scene-transitions-loader](https://github.com/excaliburjs/Excalibur/assets/612071/e81b3762-e0f9-4122-bd22-e3e477a1abd7)
  • Loading branch information
eonarheim authored Jan 14, 2024
1 parent 6f6d7bb commit bdaf7d1
Show file tree
Hide file tree
Showing 89 changed files with 2,592 additions and 688 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"no-unused-labels": "error",
"no-var": "error",
"prefer-const": "error",
"require-await": "warn",
"radix": "error",
"max-len": ["error", { "code": 140 }],
"semi": ["error", "always"],
Expand Down
44 changes: 43 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,49 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

-
- Scene Transition & Loader API, this gives you the ability to have first class support for individual scene resource loading and scene transitions.
* Add or remove scenes by constructor
* Add loaders by constructor
* New `ex.DefaultLoader` type that allows for easier custom loader creation
* New `ex.Transition` type for building custom transitions
* 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
* Scenes now can have `async onInitialize` and `async onActivate`!
* New scenes director API that allows upfront definition of scenes/transitions/loaders

* Example:
Defining scenes upfront
```typescript
const game = new ex.Engine({
scenes: {
scene1: {
scene: scene1,
transitions: {
out: new ex.FadeInOut({duration: 1000, direction: 'out', color: ex.Color.Black}),
in: new ex.FadeInOut({duration: 1000, direction: 'in'})
}
},
scene2: {
scene: scene2,
loader: ex.DefaultLoader, // Constructor only option!
transitions: {
out: new ex.FadeInOut({duration: 1000, direction: 'out'}),
in: new ex.FadeInOut({duration: 1000, direction: 'in', color: ex.Color.Black })
}
},
scene3: ex.Scene // Constructor only option!
}
})

// Specify the boot loader & first scene transition from loader
game.start('scene1',
{
inTransition: new ex.FadeInOut({duration: 500, direction: 'in', color: ex.Color.ExcaliburBlue})
loader: boot,
});
```

### Fixed

Expand Down
46 changes: 0 additions & 46 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@
"typedoc": "0.25.3",
"typescript": "5.3.3",
"url-loader": "4.1.1",
"wallaby-webpack": "3.9.16",
"webpack": "5.89.0",
"webpack-cli": "5.1.4"
},
Expand Down
22 changes: 11 additions & 11 deletions sandbox/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,16 @@ cards2.draw(game.graphicsContext, 0, 0);

jump.volume = 0.3;

var loader = new ex.Loader();
loader.addResource(heartImageSource);
loader.addResource(heartTex);
loader.addResource(imageRun);
loader.addResource(imageJump);
loader.addResource(imageBlocks);
loader.addResource(spriteFontImage);
loader.addResource(cards);
loader.addResource(cloud);
loader.addResource(jump);
var boot = new ex.Loader();
boot.addResource(heartImageSource);
boot.addResource(heartTex);
boot.addResource(imageRun);
boot.addResource(imageJump);
boot.addResource(imageBlocks);
boot.addResource(spriteFontImage);
boot.addResource(cards);
boot.addResource(cloud);
boot.addResource(jump);

// Set background color
game.backgroundColor = new ex.Color(114, 213, 224);
Expand Down Expand Up @@ -942,6 +942,6 @@ game.currentScene.camera.strategy.lockToActorAxis(player, ex.Axis.X);
game.currentScene.camera.y = 200;

// Run the mainloop
game.start(loader).then(() => {
game.start(boot).then(() => {
logger.info('All Resources have finished loading');
});
18 changes: 12 additions & 6 deletions sandbox/tests/gotoscene/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
class Scene1 extends ex.Scene {
onInitialize(_engine: ex.Engine): void {
async onInitialize(_engine: ex.Engine) {
console.log('before async');
await ex.Util.delay(1000);
console.log('after async');
const actor = new ex.Actor({
x: _engine.halfDrawWidth,
y: _engine.halfDrawHeight,
Expand All @@ -11,30 +14,33 @@ class Scene1 extends ex.Scene {

_engine.input.pointers.primary.on(
"down",
(event: ex.PointerEvent): void => {
_engine.goToScene("scene2");
async (event: ex.PointerEvent) => {
await _engine.goToScene("scene2");
}
);
}

onActivate(): void {
async onActivate() {
console.log('Scene 1 Activate')
}
}


class Scene2 extends ex.Scene {
onInitialize(_engine: ex.Engine): void {
async onInitialize(_engine: ex.Engine) {
await ex.Util.delay(1000);
// _engine.start();
const actor = new ex.Actor({
pos: ex.Vector.Zero,
width: 1000,
height: 1000,
color: ex.Color.Cyan,
});
actor.angularVelocity = 1;
_engine.add(actor);
}
onActivate(): void {
async onActivate() {
await ex.Util.delay(1000);
console.log('Scene 2 Activate')
}
}
Expand Down
12 changes: 12 additions & 0 deletions sandbox/tests/router/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>Router Test</title>
</head>
<body>
<script src="../../lib/excalibur.js"></script>
<script src="index.js"></script>
</body>
</html>
139 changes: 139 additions & 0 deletions sandbox/tests/router/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/// <reference path='../../lib/excalibur.d.ts' />
var scene1 = new ex.Scene();
scene1.add(new ex.Label({
pos: ex.vec(100, 100),
color: ex.Color.Green,
text: 'Scene 1',
z: 99
}))
var scene2 = new ex.Scene();
scene2.add(new ex.Label({
pos: ex.vec(100, 100),
color: ex.Color.Violet,
text: 'Scene 2',
z: 99
}))

class MyCustomScene extends ex.Scene {
onTransition(direction: "in" | "out") {
return new ex.FadeInOut({
direction,
color: ex.Color.Violet,
duration: 2000
});
}
onPreLoad(loader: ex.DefaultLoader): void {
const image1 = new ex.ImageSource('./spritefont.png?=1');
const image2 = new ex.ImageSource('./spritefont.png?=2');
const image3 = new ex.ImageSource('./spritefont.png?=3');
const image4 = new ex.ImageSource('./spritefont.png?=4');
const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png');
loader.addResource(image1);
loader.addResource(image2);
loader.addResource(image3);
loader.addResource(image4);
loader.addResource(sword);
}
onActivate(context: ex.SceneActivationContext<unknown>): void {
console.log(context.data);
}
}

let scenes = {
scene1: {
scene: scene1,
transitions: {
in: new ex.FadeInOut({duration: 500, direction: 'in'})
}
},
scene2: {
scene: scene2,
loader: ex.DefaultLoader,
transitions: {
out: new ex.FadeInOut({duration: 500, direction: 'out'}),
in: new ex.CrossFade({duration: 2500, direction: 'in', blockInput: true})
}
},
scene3: MyCustomScene
} satisfies ex.SceneMap<any>;

var gameWithTransitions = new ex.Engine({
width: 800,
height: 600,
displayMode: ex.DisplayMode.FitScreenAndFill,
scenes
});


var actor = new ex.Actor({
width: 100,
height: 100,
pos: ex.vec(100, 100),
color: ex.Color.Red
})
actor.addChild(new ex.Actor({
width: 100,
height: 100,
pos: ex.vec(100, 100),
color: ex.Color.Black
}));
scene1.add(actor);


scene2.onPreLoad = (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');
const image4 = new ex.ImageSource('./spritefont.png?=4');
const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png');
loader.addResource(image1);
loader.addResource(image2);
loader.addResource(image3);
loader.addResource(image4);
loader.addResource(sword);
}
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({
width: 100,
height: 100,
pos: ex.vec(400, 400),
color: ex.Color.Blue
}));

var boot = new 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');
const image4 = new ex.ImageSource('./spritefont.png?=4');
const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png');
boot.addResource(image1);
boot.addResource(image2);
boot.addResource(image3);
boot.addResource(image4);
boot.addResource(sword);
gameWithTransitions.input.keyboard.on('press', evt => {
gameWithTransitions.goto('scene3', {
sceneActivationData: { data: 1 }
});
});
gameWithTransitions.input.pointers.primary.on('down', () => {
gameWithTransitions.goto('scene1');
});
var startTransition = new ex.FadeInOut({duration: 500, direction: 'in', color: ex.Color.ExcaliburBlue});
// startTransition.events.on('kill', () => {
// console.log(game.currentScene.entities);
// console.log('killed!');
// })
gameWithTransitions.start('scene1',
{
inTransition: startTransition,
loader: boot
});
Binary file added sandbox/tests/router/spritefont.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion sandbox/tests/scene/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class MyGame2 extends ex.Engine {
console.log("scene deactivate");
}
}
onInitialize() {
async onInitialize() {
console.log("engine init");
}
}
Expand Down
Loading

0 comments on commit bdaf7d1

Please sign in to comment.