Scene transitions in ExcaliburJS - 11/11
![Scene transitions in Excalibur JS](http://nicastro.in/images/sword-adventure-tutorial/scene-transition.gif)
This is an open-source project on game development with a focus on the HTML5 Excalibur game engine. Check out this feature implementation on this commit in our Github repository!
Welcome to the 11th tutorial about HTML5 game development with the ExcaliburJS engine! You can read here the list of features we are going to develop for our Excalibur game, such as internationalization with i18next. If you're interested in game development, follow me on Twitter and Instagram to be caught up on my latest news!
Table of Contents
In this tutorial: scene transitions
In this tutorial, we add scene transitions to our game. We'll add hooks to execute code before the scene starts or ends, and for preparing it.
An application example is to show interstitial ads when changing scenes. Another could be to load specific scene resources instead of loading them at the game start. In this tutorial we'll add a simple sword animation using the animate.css library.
What we are going to do
In this tutorial we'll wrap the goToScene
Excalibur method so that to execute and wait for asynchronous code to be completed before changing the scene. In our experience, the onActivate
and onInitialize
methods of the Excalibur Scene
is not the right place, since they do not wait for promises to be completed.
Let's wrap the goToScene method
We start by going to the Game
class and modifying the goToScene
method as follows:
export class Game extends Engine {
...
public goTo({toScene, fromScene}: GoToSceneConfig): void {
this.endSceneTransitions(fromScene).then(async () => {
await this.prepareSceneAndGoto(toScene);
});
}
private async prepareSceneAndGoto(toScene: SceneKeys): Promise<void> {
const {prepareScene,sceneStart} = SceneTransitions.transitions[toScene];
await prepareScene();
sceneStart().then(()=> {
Game.getInstance().goToScene(toScene);
});
}
private endSceneTransitions(fromScene: SceneKeys): Promise<void> {
if (Util.isDefined(fromScene)) {
const {sceneEnds} = SceneTransitions.transitions[fromScene];
const currentScene = Game.getInstance().currentScene;
return sceneEnds(currentScene)
} else {
return Promise.resolve();
}
}
...
}
As you can see, when calling the goTo
method it's now possible to specify from which scene it is made the transition. The private methods endSceneTransitions
and prepareSceneAndGoto
wait for some promises
to resolve before going on.
The scene transition configuration is written in the SceneTransitions
module, which is done as follows:
export module SceneTransitions {
export const transitions: SceneTransitions = {
menuLevel: {
prepareScene: () => {
return Promise.resolve();
},
sceneStart: () => {
return Promise.resolve();
},
sceneEnds: () => {
return Promise.resolve();
}
},
playLevel: {
prepareScene: () => {
return Promise.resolve();
},
sceneStart: () => {
return Container.get(TransitionAnimationService).animateSceneTransition('playLevel');
},
sceneEnds: () => {
return Promise.resolve();
}
}
}
}
export type SceneTransitions = {[key in SceneKeys]: SceneTransition};
export type SceneTransition = {
prepareScene: () => Promise<any>,
sceneStart: (scene?: Scene) => Promise<any>
sceneEnds: (scene?: Scene) => Promise<any>
};
The only transition which is written here is the one when starting the PlayLevel
. If we wanted to have other transitions, we should only fill the corresponding field in the transition
variable of this module.
As you can see, this mechanism is simple. Let's have a look at the TransitionAnimationService
:
@Service()
export class TransitionAnimationService {
private domService: DOMService = Container.get(DOMService);
private randomService: RandomService = Container.get(RandomService);
private animationTokens: string[] = ['animate__zoomInRight', 'animate__tada', 'animate__bounce', 'animate__jello', 'animate__backInLeft', 'animate__lightSpeedInRight', 'animate__rotateInUpRight', 'animate__rollOut'];
public animateSceneTransition(sceneKey: SceneKeys): Promise<void> {
const sceneTransitionPromise: Promise<void> = firstValueFrom(sceneTransitionAnimationEnd);
this.transitionAnimation(sceneKey);
return sceneTransitionPromise;
}
private transitionAnimation(sceneKey: SceneKeys): void {
const config = TransitionsConfig.animations[sceneKey];
if(Util.isDefined(config)){
const {animationDuration} = config;
const layerIds: LayerIds[] = ['game', 'menu','joystick'];
layerIds.forEach(id => this.domService.toggleElementVisibility(id, false));
this.domService.toggleElementVisibility('animation-place', true)
const animatedElement: HTMLElement = this.domService.getElement(uiConfig.sceneTransitionDiv);
const animationToken = this.randomService.pickOne(this.animationTokens)
const listener = () => {
animatedElement.classList.remove(animationToken);
this.domService.toggleElementVisibility('animation-place', false)
animatedElement.style.removeProperty('--animate-duration');
animatedElement.removeEventListener('animationend', listener);
layerIds.forEach(id => this.domService.toggleElementVisibility(id, true));
sceneTransitionAnimationEnd.next();
};
animatedElement.addEventListener('animationend', listener);
animatedElement.style.setProperty('--animate-duration', `${animationDuration}s`);
animatedElement.classList.add(animationToken);
}
}
}
This service is following the animate.css library instructions to animate an html
element we've added in our index.html
file:
<body>
...
<div class="position-absolute position-top full-size" id="animation-place" style="visibility: hidden;">
<div class='flex--horizontal flex--justify-center flex-align-items--center gradient-background'>
<div class='animate__animated' id="scene-transition">
<img src='assets/voxel-pack/PNG/Items/sword_iron.png'/>
</div>
</div>
</div>
...
</body>
This is the result we've obtained:
That's all about scene transitions!
Conclusions
In this tutorial, we've added scene transitions to our game. In one of the next tutorials, we'll use the hooks we've added in the SceneTransitions
module to show ads on Android devices. Stay tuned!
Did you like this article and wish there were more? Donate on Paypal so that I can write more!