Creating Scenes in PixiJS

MustSeeMelons
Dec 4, 2019 · 6 min read
Photo by Kyle Head on Unsplash

Introduction

PixiJS is a wonderful library for working with 2D rendering within HTML5. It is fast, exposes a simple API and works on a multitude of devices — desktops, laptops, phones, tablets and beyond.

Should work on your smart fridge as well. Gosh, now I wish I had one.

Being a rendering engine, it can be used to create any kind of graphics experience, it is not geared towards a certain use case, like a game engine would be, let's take a look at a simple setup:

import * as PIXI from "pixi.js";
import "./resources/css/styles.css";
const MELON = require("./resources/images/watermelon.png");// Load resources
const Loader: PIXI.Loader = PIXI.Loader.shared;
Loader.add(MELON).load(setup);
const app = new PIXI.Application({
antialias: true
});
app.stage.interactive = true;document.body.appendChild(app.view);function setup() {
// create base container
const sceneContainer = new PIXI.Container();
app.stage.addChild(sceneContainer);
// create the sprite
const melonSprite = new PIXI.Sprite(
Loader.resources[MELON].texture
);
// Move the anchor to the center
melonSprite.anchor.x = 0.5;
melonSprite.anchor.y = 0.5;
// position the sprite
melonSprite.x = app.renderer.width / 2;
melonSprite.y = app.renderer.height / 2;
sceneContainer.addChild(melonSprite); // render loop
app.ticker.add(delta => {
// rotate the sprite every frame
melonSprite.rotation += 0.1 * delta;
});
}

The PixiJS API provides a callback named setup in which we create our sprites, modify their properties and optionally do something with them in the applications ticker — we have control of the render loop.

For small interactive demos it serves the purpose well, but it becomes obvious, that if we want a lot of objects some code splitting becomes necessary to keep the function lean.

What if we want to make a game? For a flappy-bird like or other simple game it would be fine as is with some functions doing the setup and updating of entities, no need to complicate things, though if we desire something a tad more complex, with multiple levels, then “scenes” start to make sense.

Simple setup repository can be found here.

What would a scene be?

It would serve the purpose of creating and updating its entities, a self-contained part of the application, in the world of video games — it would be a game level. Can also be thought of it as an act in the theatrical world.

Just one more thing is necessary — something to manage the scenes, let’s call it the engine, the flow would look like this:

logic flow

From the setup function we will now only instantiate the engine, passing in the scene configuration we wish to use.

Time to get our hands dirty.

Creating the abstractions

We will be using Typescript as that will make our abstractions clearer and more precise. We will also add in transitions between scenes, to make it a bit more fancy, onwards, to the engine!

export interface SceneTransition {
init(
app: PIXI.Application,
type: TransitionType,
sceneContainer: PIXI.Container
): void;
update(delta: number, callback: () => void): void;
}
export interface SceneSettings {
index: number;
name?: string,
gameScene: AbstractGameScene;
fadeInTransition: SceneTransition;
fadeOutTransition: SceneTransition;
}
export class Engine {
private sceneSettings: SceneSettings[];
private app: PIXI.Application;
private currentScene: SceneSettings;
constructor(
app: PIXI.Application,
scenes: SceneSettings[]
) {
this.app = app;
this.sceneSettings = scenes;
this.sceneSettings.forEach(
(sceneSettings: SceneSettings) => {
sceneSettings.gameScene.init(
this.app,
this.sceneSwitcher
);
});
// Finding the scene with the lowest index
this.currentScene = scenes.reduce((prev, curr) => {
if (prev === undefined) {
return curr;
} else {
return prev.index > curr.index ? curr : prev;
}
}, undefined);
this.setupScene(this.currentScene);
}
sceneSwitcher = (sceneName: string) => {
this.currentScene.gameScene.setFinalizing(() => {
const scene = this.sceneSettings.find(
(sceneSettings) => {
return sceneSettings.name === sceneName;
}
);
if (scene) {
this.setupScene(scene);
this.currentScene = scene;
} else {
console.error("SCENE NOT FOUND: " + sceneName);
}
});
}
setupScene(sceneSettings: SceneSettings) {
this.app.stage.removeChildren();
const sceneContainer = new PIXI.Container();
this.app.stage.addChild(sceneContainer);
const gameScene: AbstractGameScene = sceneSettings.gameScene; gameScene.setup(sceneContainer); sceneSettings.fadeInTransition.init(this.app, TransitionType.FADE_IN, sceneContainer); sceneSettings.fadeOutTransition.init(this.app, TransitionType.FADE_OUT, sceneContainer); gameScene.fadeInTransition = sceneSettings.fadeOutTransition; gameScene.fadeOutTransition = sceneSettings.fadeInTransition;
}
update(delta: number) {
this.currentScene.gameScene.update(delta);
}
}

Our engine takes in an array of game scene settings, which are later used to set up the scenes themselves, it also provides a function for switching between the scenes. The update method will be called in the PixiJS ticker, the engine will then delegate the call to the active scene.

export enum SceneState {
LOAD,
PROCESS,
FINALIZE,
DONE
}
export interface GameScene {
sceneUpdate(delta: number): void;
}
export abstract class AbstractGameScene implements GameScene {
protected sceneState: SceneState;
protected app: PIXI.Application;
protected sceneSwitcher: (sceneName: string) => void;
protected fadeInSceneTransition: SceneTransition;
protected fadeOutSceneTransition: SceneTransition;
protected sceneContainer: PIXI.Container;
private onDone: () => void;
set fadeInTransition(fadeInSceneTransition: SceneTransition) {
this.fadeInSceneTransition = fadeInSceneTransition;
}
set fadeOutTransition(fadeOutSceneTransition: SceneTransition) {
this.fadeOutSceneTransition = fadeOutSceneTransition;
}
init(
app: PIXI.Application,
sceneSwitcher: (sceneName: string) => void): void {
this.app = app;
this.sceneSwitcher = sceneSwitcher;
}
abstract setup(sceneContainer: PIXI.Container): void;
abstract preTransitionUpdate(delta: number): void;
abstract sceneUpdate(delta: number): void;
update(delta: number): void {
switch (this.sceneState) {
case SceneState.LOAD:
this.fadeInSceneTransition.update(delta, () => {
this.sceneState = SceneState.PROCESS;
});
this.preTransitionUpdate(delta);
break;
case SceneState.PROCESS:
this.sceneUpdate(delta);
break;
case SceneState.FINALIZE:
this.fadeOutSceneTransition.update(delta, () => {
this.sceneState = SceneState.DONE;
if (this.onDone) {
this.onDone();
}
});
break;
}
}
setFinalizing(onDone: () => void) {
this.onDone = onDone;
this.sceneState = SceneState.FINALIZE;
}
}

The scene itself has one core method — update for making it run, represented by the update method.

An abstract class is created to implement the scenes lifecycle: load, process, finalize, the “real” scenes will extend this and provide implementations for all the abstract methods:

  • setup — called by the engine when setting up
  • preTransitionUpdate — called while the transition is in action
  • sceneUpdate — called while the scene is active

The class is marked abstract to forbit instantiations of the class as it provides just a partial implementation — no sprites are created, that will be done in the concrete classes:

const MELON = require("../../resources/images/watermelon.png");const Loader: PIXI.Loader = PIXI.Loader.shared;export class ClockwiseScene extends AbstractGameScene {
private melon: PIXI.Sprite;
setup(sceneContainer: PIXI.Container) {
this.sceneState = SceneState.LOAD;
this.melon = new PIXI.Sprite(
Loader.resources[MELON].texture
);
this.melon.anchor.x = 0.5;
this.melon.anchor.y = 0.5;
this.melon.x = this.app.renderer.width / 2;
this.melon.y = this.app.renderer.height / 2;
this.melon.interactive = true;
this.melon.addListener("pointerup", () => {
this.sceneSwitcher("counterClockwise");
});
sceneContainer.addChild(this.melon);
}
preTransitionUpdate(delta: number) {
this.melon.rotation += 0.1 * delta;
}
sceneUpdate(delta: number) {
this.melon.rotation += 0.1 * delta;
}
}

The concrete class is responsible of adding objects and related logic, like a click listener and frame updates. With all this, our beloved setup function for Pixi itself now looks very lean:

function setup() {
const engine: Engine = new Engine(app, [
{
index: 0,
name: "clockwise",
gameScene: new ClockwiseScene(),
fadeInTransition: new SimpleFadeTransition(0.1),
fadeOutTransition: new SimpleFadeTransition()
},
{
index: 1,
name: "counterClockwise",
gameScene: new CounterClockwiseScene(),
fadeInTransition: new SimpleFadeTransition(0.1),
fadeOutTransition: new SimpleFadeTransition()
}]);
app.ticker.add(delta => {
engine.update(delta);
});
}

We create the scenes and together with some configuration, pass them to the engine. The engine will then setup the correct scene when necessary.

Note: the setup function has to clear any previous state.

An extra scene was added, one which rotates the melon in the opposite direction, the resulting behavior can be seen in the gif below (sorry, it is not a perfect loop):

a beautiful, rotating melon

Conclusion

With a few abstraction we have created the infrastructure to manage more complex Pixi projects by splitting responsibilities to different objects.

There are a few things that could be improved:

  • Scene pooling — no need to re-setup scenes everytime, if we switch between them a lot, rooms in an RPG for example
  • Re-usable transitions — currently they recreated, and there is one for fade-in, fade-out, would be a good optimization
  • More lifecycle hooks — a post transition update, while we are fading out the current scene (would be very similar to the pre hook)

The repository for the finished project can be found here, cheers.

I’m sorry I called watermelon melons.

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade