If you are seeking an easy way to get started on a new Phaser game the ES2015 way, then this guide is for you. In this guide we will first use jspm to quickly set up a new project with a module loader and an ES2015 transpiler. We will then pull the latest Phaser package from the jspm registry and set up a basic game that highlights a few ways we can take advantage of ES2015 to make our code more modular. Finally, we will bundle the application so that it can be deployed as a static file.
The skeleton we create here is available on GitHub for you to use to start your own projects.
Install Node.js
If you don’t already have node.js then install it, because the next step requires npm.
Install jspm globally
jspm is a browser-side package manager that will not only help us download Phaser, but will provide us with a module loader polyfill integrated with a transpiler. This means we will be able to take advantage of ES2015 features, even though browsers are still on ES5. We will even be able to import Phaser objects just as if it was an ES2015 module!
npm install jspm -g
Create a new project
Let’s create a new project in a directory called phaser-es2015
cd phaser-es2015
Now we will install jspm as a development-only dependency of our project. Installing globally gave us the command-line interface, but jspm recommends a local install as well. This way our projects will be using the version of jspm they expect, even if it is older than the global version.
npm init
Say yes to the default options.
npm install jspm --save-dev jspm init
Say yes to the default options.
Install Phaser
jspm install phaser
Create an entry point
Create a new index.html file at the root of the project.
<!doctype html> <html> <head> <title>Phaser: The ES2015 Way</title> </head> <body> <main> <div id="game-canvas"></div> </main> </body> <script src="jspm_packages/system.js"></script> <script src="config.js"></script> <script> System.import('src/main.js'); </script> </html>
The System.import()
function dynamically loads and executes the main.js file. It becomes the entry point to our game. From this point on, we can load modules statically using the import
and export
keywords.
Now, at the root of the project we create a src directory and add a main.js.
// src/main.js import { Game } from './game'; new Game();
This file loads the module from src/game.js which is our Phaser game. If you wish to run other JavaScript on the page prior to starting the game, this is a good place to do it. Next, create the src/game.js file
Create the Game
// src/game.js import Phaser from 'phaser'; import { Load } from './states/load'; import { Menu } from './states/menu'; import { Play } from './states/play'; import { GameOver } from './states/gameover'; import { Victory } from './states/victory'; export class Game extends Phaser.Game { constructor() { super(800, 600, Phaser.AUTO, 'phaser-canvas', { create: () => { this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; this.state.add('load', Load); this.state.add('menu', Menu); this.state.add('play', Play); this.state.add('gameover', GameOver); this.state.add('victory', Victory); this.state.start('load'); }}); }
The Game
class extends Phaser.Game
and is responsible for adding all of the states that the game will transition into. It also starts the initial state. It is often a good idea to create a load state as your initial state and use it to load all of the required audio, images, and other media. While these things are loading the load state can display a loading message so the user knows that your game is not frozen. A typical game may also have a menu state, a state for game play, a victory state,
and a game over state.
Why does the Phaser import not use { }?
When a module exports something by name, we use this syntax to import it:
import { namedThing } from "myModule";
If we want to change the name, we can do it like this:
import { namedThing as myThing } from "myModule";
A module can also specify a nameless default export. In that case, we use this syntax:
import myThing from "module";
There was a change made to the phaser jspm registry entry to set the Phaser object as the default export. Before that change was made, SystemJS did export detection automatically, creating a named export called “Phaser”. This was a very frustrating change because it broke all of my code that had been using the named export from Phaser 2.2.2 onward!
Special thanks to @vemek for noticing that the imports in the skeleton stopped working!
Create the States
All of our states extend Phaser.State
and are in different files.
Load State
The load state described below is the first state and displays the message “Loading…” while the assets are loaded. Until you add more assets, it is unlikely you will actually notice this screen when you start the game. Once finished, it loads the menu state.
// src/states/load.js import Phaser from 'phaser'; export class Load extends Phaser.State { preload() { let textStyle = {font: '45px Arial', alight: 'center', stroke: 'blue', fill: 'blue'}; this.game.add.text(80, 150, 'loading...', textStyle); this.game.load.spritesheet('wizard', '../assets/wizardsprite.png', 95, 123, 6); } create() { this.game.state.start('menu'); } }
Menu State
Most games will also want a menu screen. For this skeleton the menu just has some instructions to read before pressing ‘s’ to start the game. The object of the game is to press spacebar before time runs out. Very exciting!
// src/states/menu.js import Phaser from 'phaser'; export class Menu extends Phaser.State{ create() { let textStyle = {font: '45px Arial', alight: 'center', stroke: 'blue', fill: 'blue'}; let title = this.game.add.text(this.game.world.centerX, this.game.world.centerY - 100, 'ES2015 Wizard', textStyle); title.anchor.set(0.5); textStyle.font = '36px Arial'; let instructions = this.game.add.text(this.game.world.centerX, this.game.world.centerY, '"s" key to start', textStyle); instructions.anchor.set(0.5); let controlMessage = this.game.add.text(this.game.world.centerX, this.game.world.centerY + 150, 'use arrow keys to move', textStyle); controlMessage.anchor.set(0.5); let muteMessage = this.game.add.text(this.game.world.centerX, this.game.world.centerY + 225, '"SPACEBAR" to win.', textStyle); muteMessage.anchor.set(0.5); let sKey = this.game.input.keyboard.addKey(Phaser.KeyCode.S); sKey.onDown.addOnce( () => this.game.state.start('play')); } }
Play State
Once ‘s’ is pressed from the menu state we transition to the play state. Here we add a simple wizard sprite. The logic related to the wizard’s movement will be covered later, as it is encapsulated in the wizard module. If 20 seconds pass without pressing spacebar we transition to gameover state. If spacebar is pressed in time we transition to the victory state.
// src/states/play.js import Phaser from 'phaser'; import { Wizard } from '../sprites/wizard';; export class Play extends Phaser.State { create() { this.game.physics.startSystem(Phaser.Physics.ARCADE); this.wizard = new Wizard(this.game, 350, 300); this.game.add.existing(this.wizard); this.cursors = this.game.input.keyboard.createCursorKeys(); this.game.stage.backgroundColor = "#FFFFFF"; this.game.input.keyboard.addKeyCapture([ Phaser.KeyCode.SPACEBAR ]); } update() { if (!this.startTime) { this.startTime = Date.now(); } //20 seconds to win if ((Date.now() - this.startTime) > 20000) { this.startTime = 0; this.game.state.start('gameover'); } this.wizard.move(this.cursors); if (this.game.input.keyboard.isDown(Phaser.KeyCode.SPACEBAR)) { this.startTime = 0; this.game.state.start('victory'); } } }
Victory State
The victory state simply informs the user that she has won, and to press ‘s’ to play again. Pressing ‘s’ transitions the game back into the play state.
// src/states/victory.js import Phaser from 'phaser'; export class Victory extends Phaser.State{ create() { let textStyle = {font: '45px Arial', alight: 'center', stroke: 'black', fill: 'red'}; let title = this.game.add.text(this.game.world.centerX, this.game.world.centerY - 100, 'Victory!', textStyle); title.anchor.set(0.5); textStyle.font = '36px Arial'; let instructions = this.game.add.text(this.game.world.centerX, this.game.world.centerY, '"s" key to play again', textStyle); instructions.anchor.set(0.5); let sKey = this.game.input.keyboard.addKey(Phaser.KeyCode.S); sKey.onDown.addOnce( () => this.game.state.start('play')); } }
Gameover State
The gameover state is similar to the victory state, except it tells the user that she has lost.
// src/states/gameover.js import Phaser from 'phaser'; export class GameOver extends Phaser.State{ create() { let textStyle = {font: '45px Arial', alight: 'center', stroke: 'red', fill: 'red'}; let title = this.game.add.text(this.game.world.centerX, this.game.world.centerY - 100, 'GAME OVER', textStyle); title.anchor.set(0.5); textStyle.font = '36px Arial'; let instructions = this.game.add.text(this.game.world.centerX, this.game.world.centerY, '"s" key to play again', textStyle); instructions.anchor.set(0.5); let sKey = this.game.input.keyboard.addKey(Phaser.KeyCode.S); sKey.onDown.addOnce( () => this.game.state.start('play')); } }
Create the Sprite
One advantage of using ES2015, is that it is easy to encapsulate each sprite’s logic into its own module. In this skeleton we choose to put them in a sprites directory. In this case we have a single wizard sprite module that sets up the animations and physics upon construction. The wizard sprite defines a move method that can be called during the game update loop in order to enable keyboard movement.
// src/sprites/wizard.js import Phaser from 'phaser'; export class Wizard extends Phaser.Sprite { constructor(game, x, y) { super(game, x, y, 'wizard'); this.anchor.setTo(0.5, 0.5); this.scale.setTo(0.65, 0.65); this.animations.add('right', [0,1,2]); this.animations.add('left', [3,4,5]); this.game.physics.enable(this, Phaser.Physics.ARCADE); this.body.drag.set(100); this.body.maxVelocity.set(500); this.body.collideWorldBounds = true; this.body.width -= 32; this.body.height -= 32; } move(cursors) { if (cursors.up.isDown) { this.body.acceleration.y = -300; } else if (cursors.down.isDown) { this.body.acceleration.y = 300; } else { this.body.acceleration.y = 0; } if (cursors.left.isDown) { this.body.acceleration.x = -300; this.animations.play('left', 4, true); } else if (cursors.right.isDown) { this.body.acceleration.x = 300; this.animations.play('right', 4, true); } else { this.body.acceleration.x = 0; } } }
Create the npm start script to run the game
Next, we modify the packages.json to add a start script that kicks of a local http server that will host our game. The -o
option tells it to open a browser window automatically, the c
option disables caching by setting the cache time to -1
. This is to prevent the browser from holding on to old code after we make changes.
To start the game go to the project root directory an execute the command
npm start
{ "name": "phaser-es2015", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "http-server -o -c-1" }, "author": "", "license": "MIT", "devDependencies": { "jspm": "^0.16.34" }, "jspm": { "dependencies": { "phaser": "github:photonstorm/phaser@^2.4.8" }, "devDependencies": { "babel": "npm:babel-core@^5.8.24", "babel-runtime": "npm:babel-runtime@^5.8.24", "core-js": "npm:core-js@^1.1.4" } } }
Create the npm build script to bundle the game
As the game gets larger, it will probably start to load too slowly. Bundling the game into a single, minified, file will reduce the number and size of HTTP requests the browser must make. Bundling should make a huge difference.
jspm can do this for us with the following command from the root directory
jspm bundle src/main.js game-bundle.js --minify
This will create a new file called game-bundle.js that we must load after system.js/config.js, but before the first import.
<!doctype html> <html> <head> <title>Phaser: The ES2015 Way</title> </head> <body> <main> <div id="game-canvas"></div> </main> </body> <script src="jspm_packages/system.js"></script> <script src="config.js"></script> <script src='./game-bundle.js'></script>; <script> System.import('src/main.js'); </script> </html>
To save key strokes we can add the build command to packages.json as well.
"scripts": { "start": "http-server -o -c-1", "build": "jspm bundle src/main.js game-bundle.js --minify" },
The start script is special in npm and works with the short-hand command
npm start
But this will not work for build, the command must be
npm run build
Conclusion
Although this setup appears to be longer in lines of code than many other single-file Phaser samples, I have found that with this modular approach, adding complexity to the game comes naturally and doesn’t require as much refactoring along the way.
Billy says
Thanks for the tutorial and code.
For some reason I can’t get the http-server running. I assume that needs to be installed via npm?
Thanks!
Alexey Romanov says
@Billy:
Yes, you can install http-server globally:
npm install -g http-server