Сreating PixiJS project template
In this article, our goal is to develop a simple project template that we will use as a foundation for all our future games. You can check the final template code in GitHub.
Video version
Step-by-step guide on Udemy
Template functionality:
- Application creation and launch
- Resource loading and rendering
- Management of scenes, states, animations, sounds and screen
1. Creating the structure
Let's take an empty project structure from the initial branch and examine it.
1.1 Document template
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<style>
body {
background-color: #000;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
canvas {
position:absolute;
top:50%;
left:50%;
transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
}
</style>
<body>
</body>
</html>
The HTML template will contain the final JavaScript code after the game is built. Let’s style the canvas element and leave the body
tag empty.
1.2 Project dependencies.
package.json
{
"name": "match3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack/prod.js ",
"start": "webpack-dev-server --config webpack/base.js --open"
},
"dependencies": {
"gsap": "^3.10.4",
"pixi.js": "^6.5.1"
},
"devDependencies": {
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.8.0"
}
}
Config contains project settings and dependencies. Main dependencies are:
1.3 Project structure
webpack/
- build scriptssrc/scripts/system/
- common system code, the same for all gamessrc/scripts/game/
- code of the game itself, unique for each projectsrc/sounds/
- audio assetssrc/sprites/
- images assets
1.4 Commands
First we need to install dependencies from package.json
As you can see in the scripts block in package.json
, we have 2 ways to run webpack: build and start.
This command creates the build with the developer environment and automatically run the project in the browser using dev-server.
This command creates the build for production and save the created build's files in the dist/
folder.
This folder is created automatically and cleared every time we run the command, so be careful not to save files in this folder.
2. Creating the canvas element
Create a file src/scripts/system/App.js
. This is the main class of the application.
Let's implement the run
method, which will start the game.
import * as PIXI from "pixi.js";
class Application {
run(config) {
this.config = config;
this.app = new PIXI.Application({resizeTo: window});
document.body.appendChild(this.app.view);
}
}
export const App = new Application();
Here we import the PIXI
library and create a PIXI
application.
The view
property of the this.app
object is just the HTML canvas
element that we add to the DOM structure of our html template.
In addition, we make this class a singleton by creating an instance of the class and exporting the created object, not the class itself.
So wherever in the application we import from this file, we will always get the same App
object. This way using the global App
object we can get access to all the necessary classes for managing the game: screen manager, sound manager, scene manager, and so on.
Create an entry point src/scripts/index.js
and run the application:
3. Creating the loader
Let's start with the loader interface that we want to implement.
class Application {
run(config) {
// …
this.loader = new Loader(this.app.loader, this.config);
this.loader.preload().then(() => this.start());
}
start() {
}
To load resources, PIXI
provides us with the PIXI.Loader
class. We can get it from the app
property.
We pass it as the first parameter to the constructor of our custom Loader
class.
And the second parameter is a list of resources to download.
Create class src/scripts/system/Loader.js
:
export class Loader {
constructor(loader, config) {
this.loader = loader;
this.config = config;
this.resources = {};
}
preload() {
return Promise.resolve();
}
}
resources
property which is empty by default.
Since the list of resources will be unique for each game, we need to define the resource config separately from the general code, that is, outside the system
folder.
Let's create a game/Config.js
file. Here we create the Config
object, which will be unique for each specific game:
import { Tools } from "../system/Tools";
export const Config = {
loader: Tools.massiveRequire(require["context"]('./../../sprites/', true, /\.(mp3|png|jpe?g)$/))
};
Let's set the list of resources to load in the loader
property.
To automatically get the entire list of resources to load from a given folder, we use the capabilities of require.context
.
Let's create the Tools
system class and implement the massiveRequre
method in it:
export class Tools {
static massiveRequire(req) {
const files = [];
req.keys().forEach(key => {
files.push({
key, data: req(key)
});
});
return files;
}
}
And now we need to update the entry point:
Now we can fully implement the preload
method in the Loader
class. We can do it in 2 steps:
- Add all resources from the loader config to the loading list using
this.loader.add
method. - Start loading resources using
this.loader.load
.
export class Loader {
// ...
preload() {
for (const asset of this.config.loader) {
let key = asset.key.substr(asset.key.lastIndexOf('/') + 1);
key = key.substring(0, key.indexOf('.'));
if (asset.key.indexOf(".png") !== -1 || asset.key.indexOf(".jpg") !== -1) {
this.loader.add(key, asset.data.default)
}
}
return new Promise(resolve => {
this.loader.load((loader, resources) => {
this.resources = resources;
resolve();
});
});
}
}
The load
method of the PIXI.Loader
object takes a callback function as a parameter, which will be called when all the resources have finished loading and become available for use.
The callback function takes 2 parameters: the loader object itself and the second parameter is the loaded resources. Let's put them in the resources
field in the Loader
class, which we specially reserved for all loaded resources.
4. Game launch
In the Application
class, we implement the start
method, which will start the game after the resources are loaded:
// ...
class Application {
// ...
start() {
this.scene = new this.config["startScene"]();
this.app.stage.addChild(this.scene.container);
}
We could instantiate the scene class directly in the start
method. But we want the shared code in the system
folder to be unrelated to or dependent on the game code in the game
folder. To do this, we have separated the common system code and the project code. At the same time, the system code can know about the parameters it needs through the game config, which we pass to the App
class when launching applications. So in this case, instead of directly creating the game scene object directly in the Application
class, we'd better create it through a parameter in the config.
Add the startScene
parameter to the game config in Config.js
:
And create the Game
class itself in game folder /src/scripts/game/Game
:
import * as PIXI from "pixi.js";
import { App } from "../system/App";
export class Game {
constructor() {
this.container = new PIXI.Container();
}
}
The scene class is based on the PIXI.Container
. And we will add all objects added to the scene to this container.
And we added the scene container itself to the main app.stage
container in the start method of the Application
class.
5. Sprite Output
To render sprites, we need to implement a helper method in the Application
class:
res(key) {
return this.loader.resources[key].texture;
}
sprite(key) {
return new PIXI.Sprite(this.res(key));
}
We know that all loaded resources are stored in the resources
property of our custom Loader
class. Getting the required resource by key, we can create a new instance of the PIXI.Sprite
class.
Now, in the code of the game, it will be enough for us to use only the call to the App.sprite
method to get the required PIXI.Sprite
instance and work with it further.
Let's render the background image:
export class Game {
constructor() {
this.container = new PIXI.Container();
this.createBackground();
}
createBackground() {
this.bg = App.sprite("bg");
this.bg.width = window.innerWidth;
this.bg.height = window.innerHeight;
this.container.addChild(this.bg);
}
6. Scenes Manager
Let's create a scene manager for easy switching between scenes in the game. Let's create a base scene class:
import * as PIXI from "pixi.js";
import { App } from "./App";
export class Scene {
constructor() {
this.container = new PIXI.Container();
this.container.interactive = true;
this.create();
App.app.ticker.add(this.update, this);
}
create() {}
update() {}
destroy() {}
remove() {
App.app.ticker.remove(this.update, this);
this.destroy();
this.container.destroy();
}
}
Let's add 3 methods to the base class that can be overridden in the project scene:
- create
- update
- destroy
In the constructor, we will perform the universal actions required for each scene in the game:
- create a scene container
- call the create
method, which will be overridden in the game scene
- add PIXI
ticker with update
method so that it is called on every animation frame
- add the remove
method that will be called by the manager when the scene is destroyed and implement the deletion of the ticker in it
Now let's create the manager itself to manage and switch scenes:
import * as PIXI from "pixi.js";
import { App } from "./App";
export class ScenesManager {
constructor() {
this.container = new PIXI.Container();
this.container.interactive = true;
this.scene = null;
}
start(scene) {
if (this.scene) {
this.scene.remove();
}
this.scene = new App.config.scenes[scene]();
this.container.addChild(this.scene.container);
}
}
start
method.
Which scene class corresponds to this key, we specify in the global config:
Let's finalize our game scene Game
by making this class an inheritor of the base class of the scene:
And all that's left is to change the scene launch method in the App.js
application class:
// ...
import { ScenesManager } from "./ScenesManager";
class Application {
run(config) {
//...
this.scenes = new ScenesManager();
this.app.stage.addChild(this.scenes.container);
}
// ...
start() {
this.scenes.start("Game");
}
}