Article

HTML5 Game Development with Phaser

Adapted from the presentation “HTML5 Game Development With Phaser”, delivered to the Fredericksburg Developer group on September 10, 2019

Game development for the web can be challenging. The Phaser framework offers a complete API for developing <canvas> games in a straightforward and well organized environment. In this article we’ll look at the core features of phaser and use them to create a clone of the classic nintendo arcade game “Mario Bros.”

Let’s Make a Game

There are three methods for adding Phaser to your project. The easiest is to grab the url for their CDN and attach it to your page with a script tag. If you don’t want to rely on a third party CDN, you can download the files and use a script tag. The third method, used in our sample project, installs the Phaser npm package, and bundles it into the project with webpack.

This tutorial assumes that you’re already familiar with npm and webpack. If not, you’ll first need to install and set up npm on your computer. When you’re ready, grab a copy of the Phaser Game project: Zeta Bros. on GitHub

Let’s get started.

Step 1: Installing Phaser

When starting a new npm project, you can use the install command to download and then save the Phaser package:npm install — save phaser

The Zeta Bros. project already has Phaser, webpack and several other packages saved already. Run the install command to download all of the packages used in this project.

npm install

The project is already set up with its own dev server. After installing, you can run the start command to spin up the server and try out the project in your browser.

npm start

The Zeta Bros. repo on GitHub has a series of branches labeled “Step{number}” which incrementally add to the source code in building the game. This article is formatted to follow these branches. It explains the changes that are added in each step of the process. Use the branches to follow along in this article as the game is built. If you want to jump around, use the master branch to see the complete game and use this article as a guide.

Step 2: Game Object

Setting up a Phaser game is a very straightforward process. If you’re using the npm package, you’ll first need to import the Phaser library. This is done by adding an import statement to the top of your main script file. In the Zeta Bros. project open the file src/main.js and let’s take a closer look.

Below the phaser import statement, you’ll notice a second import for `GameScene`, we’ll get to that in the next step. Phaser accepts a configuration object upon instantiation. For this project we’re using mostly the defaults but some are written out for clarity:

  • ‘type’ indicates whether or not Phaser will use WebGL with hardware acceleration.
  • ‘width’, ‘height’, and ‘parent’ define the dimensions of the <canvas> element and the parent element to attach it to.
  • ‘physics’ defines which physics engine you are using, phaser has several, and for this project we’ll use ‘arcade’ physics which is good enough for simple games and gives us the advantage of speed on less capable platforms.
  • And finally, the ‘scene’ array which contains references to all of your game scenes, for now we just have one.

To instantiate a Phaser game object, simply declare a new Phaser.Game object and pass it the configuration object.

import ‘phaser’;
import { GameScene } from ‘./scenes/GameScene’;
const gameConfig = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  parent: ‘phaser-game’,
  dom: {
    createContainer: true
  },
  physics: {
    default: ‘arcade’,
    arcade: {
      gravity: { y: 900 },
      debug: false
    }
  },
  scene: [
    GameScene
  ]
};
new Phaser.Game(gameConfig);

Step 3: Adding a Background Image

Remember the GameScene object that was imported in the last step? Let’s take a closer look at it. It extends Phaser.Scene and overloads three methods that most Scene objects use: `preload()` which is called first and used to preload assets, `create()` which is called every time the scene loads so you want to be careful about which values you initialize here, and `update()` which is called on each iteration of the main animation loop.

Here we use the scene’s `load()` method to preload an image. It’s then added to the scene using the scene’s `add()` method. When any asset is loaded it’s assigned a key value, in this case ‘background’. Keys are global so be careful that your names don’t conflict with each other.

import Phaser from ‘phaser’;
export class GameScene extends Phaser.Scene {
  constructor() {
    super({
      key: ‘GameScene’
    });
  }
  preload() {
    this.load.image(‘background’, ‘assets/images/background.png’);
  }
  create() {
    // Add the background image
    this.add.image(400, 300, ‘background’);
  }
  update() {
  }
}

Step 4: Add a Player Character

For the player character we’re going to load a sprite sheet. Sprite sheets are simply a collection of identically sized images, called “sprites”. Sprites in the sprite sheet are aligned in a grid pattern making up a single large image. By telling Phaser the size of the smaller images (frames), it will automatically break up the large image into its individual sprites.

this.load.spritesheet(‘zeta’,
  ‘assets/images/zeta_spritesheet.png’,
  { frameWidth: 40, frameHeight: 60 }
);

For convenience, a `createPlayer()` method has been added in the Zeta Bros. project.

this.player = this.createPlayer();

In createPlayer we use a factory function from the arcade physics engine `add.sprite()`, to create a Sprite game object for us. You could of course instantiate your own Sprite object. But using the factory has the advantage of attaching the Sprite object to the Scene, and the Physics engine, in a single function call.

Additionally, some physics properties are added. `setBounce()` adds a little bit of a bounce on collisions and `setCollideWorldBounds()` prevents the object from ever leaving the canvas area no matter how hard it’s hit.

createPlayer() {
  let player = this.physics.add.sprite(100, 450, ‘zeta’);
  player.setBounce(0.2);
  player.setCollideWorldBounds(true);
  return player;
}

Step 5: Add Simple Keyboard Controls

Phaser also abstracts player controls. This project uses simple keyboard controls but Phaser allows us to easily swap out other control schemes like game pads or touch screens using the same code.

this.cursors = this.input.keyboard.createCursorKeys();

Inside the update method we’re able to check for directional commands in the cursors object we created. This game only uses three buttons: left, right and up for jumping.

if (this.cursors.left.isDown){
  this.player.setVelocityX(-200);
}else if (this.cursors.right.isDown){
  this.player.setVelocityX(200);
}else{
  this.player.setVelocityX(0);
}
if (this.cursors.up.isDown && this.player.body.touching.down){
  this.player.setVelocityY(-750);
}

You might have noticed the additional check for `this.player.body.touching.down` when jumping. It ensures the player is touching the ground before they’re allowed to jump. For now we don’t have a ground, so your player won’t be able to jump. Don’t worry, we’ll add one soon.

Step 6: Define Character Animation

If you’ve been following along in the repo branches, at this point you have a character that you can move around the screen, however it’s not very animated. By taking advantage of the sprite sheet we can define each of the individual images as frames in an animation and then call them where appropriate.

For convenience, an `initAnimation()` method has been added which defines the animation frames.

this.initAnimation();

In initAnimation the scene’s `anims.create()` method is used to define a animation. Like all other assets animations are assigned a key value. In addition, a list of sprite frames are provided to define the animation. Here we define the “left” and “right” walking animations as well as an intermediate “turn” animation which shows the character standing and facing the player.

initAnimation() {
  this.anims.create({
    key: ‘left’,
    frames: this.anims.generateFrameNumbers(‘zeta’, { start: 0, end: 3 }),
    frameRate: 30,
    repeat: -1
  });
  this.anims.create({
    key: ‘turn’,
    frames: [ { key: ‘zeta’, frame: 4 } ],
    frameRate: 30
  });
  this.anims.create({
    key: ‘right’,
    frames: this.anims.generateFrameNumbers(‘zeta’, { start: 5, end: 8 }),
    frameRate: 30,
    repeat: -1
  });
}

With the animations defined, we can now assign them to the player object when moving. This is done by calling the `anims.play()` method which is available to all Sprite objects. Here we call the player object’s anims.play() and pass the appropriate animation key. The second parameter tells the animation not to play if already playing. This prevents the animation from continually resetting back to frame 1 as we hold down the directional keys.

if (this.cursors.left.isDown){
  this.player.setVelocityX(-200);
  this.player.anims.play(‘left’, true);
}else if (this.cursors.right.isDown){
  this.player.setVelocityX(200);
  this.player.anims.play(‘right’, true);
}else{
  this.player.setVelocityX(0);
  this.player.anims.play(‘turn’);
}

Step 7: Add Some Platforms

Now that we have a walking player character, it’s time to give him something to walk on. We’re going to preload a few more images just as we did the background image.

this.load.image(‘ground’, ‘assets/images/ground.png’);
this.load.image(‘wall’, ‘assets/images/wall.png’);
this.load.image(‘platform’, ‘assets/images/platform.png’);

A new method, `createPlatforms()` has been added for convenience.

this.platforms = this.createPlatforms();

the createPlatforms method uses the physics engines factory to create a new “Static Group” object. A group object allows us to attach the same physical properties to all of the objects within it. The fact that the group is “static” means objects will react to it but it will not react to objects. In other words, the floor won’t fly in the opposite direction when our character lands on it.

createPlatforms() {
  let platforms = this.physics.add.staticGroup();
  platforms.create(400, 270, ‘platform’);
  platforms.create(400, 572, ‘ground’);
  return platforms;
}

Finally, a collider is added. It receives both the player and the entire platforms group. This tells the physics engine to handle collisions between the player and each object in the group.

this.physics.add.collider(this.player, this.platforms);

Step 8: Enemy Bot

Now that we have an environment to jump around. It’s time to add some adversity in the form of high tech security robots. Adding the bots follows the same process we’ve used already:

  • Preload the image assets
  • Create a group
  • Create a collider for the group
  • Create sprites and attach them to the group.
this.load.image(‘bot’, ‘assets/images/security_bot.png’);
this.bots = this.physics.add.group();
this.physics.add.collider(this.bots, this.platforms);
this.spawnBot();

Once a bot is created, its `setVelocityX()` method is called to move it through the scene under its own power.

spawnBot(){
  this.bots.create(100, 75, ‘bot’).setVelocityX(100);
}

Steps 9&10: Collision Handlers

Without setting `setCollideWorldBounds()` for each bot, it will roll right off the screen. The problem with `setCollideWorldBounds()` is that it doesn’t register as a collision and we need a way of turning the bot around. Some wall sprites are necessary to provide a collision in the same way as the platforms.

this.walls = this.createWalls();
createWalls() {
  let walls = this.physics.add.staticGroup();
  walls.create(0, 300, ‘wall’).setActive(true);
  walls.create(800, 300, ‘wall’).setActive(true);
  return walls;
}

Another `staticGroup` is created for the walls. You may wonder why we don’t add the walls to the platforms group. The reason is that we need to attach a special collision handler that executes when a bot hits a wall. The handler is a function which is passed as the third parameter to the `add.collider()` method. Below we set up two collision handlers. The first is between the bots and the walls, which reverses the bot’s direction. And the other is between the bots and the player. Normally you would want to apply some sort of damage to the player when colliding with an enemy. But for this demo we’ll end the game.

this.physics.add.collider(this.bots, this.walls, this.handleCollisionWall.bind(this));
this.physics.add.collider(this.player, this.bots, this.handleCollisionEnemy.bind(this));
handleCollisionWall(event, collider){
  if(collider.body.touching.left){
    event.setVelocityX(-100);
  }else if(collider.body.touching.right){
    event.setVelocityX(100);
  }
  return true;
}
handleCollisionEnemy(event, collider) {
  this.scene.start(‘EndScene’);
}

Step 11: End Scene

In the last step we called `scene.start()` to load up a new scene called ‘EndScene’. To make this scene available in the game, it must be added to the scene array in the game config.

src/main.js

import { EndScene } from ‘./scenes/EndScene’;

scene: [
  GameScene,
  EndScene
]

The End scene itself is very simple, it loads and displays an image the same way we did for the game background in Step 3. With the image added, we can call its `setInteractive()` method. This allows us to attach a click event handler. The click handler calls `scene.start()` to restart the game.

src/scenes/EndScene.js

import Phaser from ‘phaser’;
export class EndScene extends Phaser.Scene {
  constructor() {
    super({
      key: ‘EndScene’
    });
  }
  preload() {
    this.load.image(‘end_splash’, ‘assets/images/end_splash.png’);
  }
  create() {
    // Add the background image
    this.splash = this.add.image(400, 300, ‘end_splash’);
    this.splash.setInteractive().on(‘pointerdown’, () => {
      this.scene.start(‘IntroScene’);
    });
  }
}

Step 12: Door Switch

With enemies in place and a way to lose, all that’s missing is a goal to achieve. For that we’re going to add an exit door and an inconveniently located switch. The switch deactivates the security bots, allowing them to be destroyed.

The door and switch are more sprite sheets, added the same way as the player in Step 4. To allow the player to press the door switch, an overlap handler is added. Overlap works the same way as a collider with a handler function, except the objects do not physically react with each other.

src/scenes/GameScene.js

this.levelCt = 1;
this.load.spritesheet(‘door’,
  ‘assets/images/door.png’,
  { frameWidth: 64, frameHeight: 64 });

this.door = this.createDoors();
this.doorsign = this.createDoorSign();

this.physics.add.overlap(this.player, this.door, this.handleOverlapDoor.bind(this));
this.doorsign.setText(this.levelCt);

createDoors(){
  let door = this.physics.add.staticGroup();
  // Entry door is inactive
  door.create(400, 508, ‘door’, 0, true, false);
  // Add an exit door
  door.create(700, 508, ‘door’, 3);
  return door;
}
createDoorSign(){
  this.add.text(380, 410, ‘LEVEL’, { fontSize: ‘12px’, fill: ‘#ff0000’, align: ‘center’, fontFamily: ‘sans-serif’ });
  return this.add.text(385, 420, ‘0’, { fontSize: ‘48px’, fill: ‘#ff0000’, align: ‘center’, fontFamily: ‘sans-serif’ });
}

At this point the `handleCollisionEnemy()` method needs to be modified slightly. First, it needs to destroy the bot when disabled. Then, when all bots are destroyed, it needs to trigger the door open animation. The door overlap handler can then load the next scene. It does so by calling the `scene.start()` method and passing a call to get the index of the next scene: `this.scene.manager.getAt(this.scene.getIndex()+1)`. Of course, the next scene is the end game screen but we’ll build on that next.

handleCollisionEnemy(player, bot) {
  if(bot.active){
    this.scene.start(‘EndScene’);
  }else{
    let tmpbot = this.physics.add.staticSprite(bot.x, bot.y, ‘bot’);
    tmpbot.anims.play(‘botAsplode’);
    bot.destroy();
    this.botKillCount++;
  }
}
handleOverlapDoor(event, collider) {
  if(collider.active){
    // Load the next scene
    this.scene.start( 
      this.scene.manager.getAt(this.scene.getIndex()+1) );
  }
}

Step 13: Level System

Running through the same scene every time can get a little boring. Fortunately, we’ve constructed this game for easy level building. All of the convenience functions that make up the GameScene object can now be overloaded for individual levels. Let’s add some new scene objects to the project. The Level1 and Level2 objects inherit our GameScene and build the levels of the game. GameScene is removed from the scenes array and replaced with Level1 and Level2. Remember when we loaded the next scene in the array when interacting with the door? This allows the scene array to control the flow of the game. In this example Level1 loads Level2 followed by EndScene. This allows you to easily add, remove and rearrange your levels just by changing the scene array.

src/main.js

import { Level1 } from ‘./levels/level1’;
import { Level2 } from ‘./levels/level2’;

scene: [
  Level1,
  Level2,
  EndScene
]

Because Level1 and Level2 extend GameScene, they immediately work as exact copies of the original GameScene. All that remains is some customizing. We can start by overloading the `createPlatforms()` and `createDoors()` methods from the GameScene object and placing platforms doors and switches at different locations. The call to `spawnBot()` has been removed from the GameScene object and is now done in the individual levels so their location and spawn rate can be customized.

src/levels/Level1.js

import { GameScene } from ‘../scenes/GameScene’;
export class Level1 extends GameScene{
  constructor() {
    super({ key: ‘Level1’ });
    this.levelCt = 1;
  }
  createPlatforms() {
    let platforms = super.createPlatforms();
    platforms.create(100, 180, ‘platform’);
    platforms.create(500, 180, ‘platform’);
    platforms.create(700, 360, ‘platform’);
    platforms.create(300, 360, ‘platform’);
    return platforms;
  }
  createDoors(){
    let door = super.createDoors();
    // Add an exit door
    door.create(100, 132, ‘door’, 3);
    return door;
  }
  create() {
    super.create();
    this.time.addEvent({
      delay: 5000,
      callback: (event)=>{
        this.spawnBot(‘left’);
      },
      callbackScope: this,
      repeat: 1
    });
  }
}

Step14 (master branch): Splash Screen

Now that our game is complete it’s time to put on a final coat of polish before going to production. Remember that ‘IntroScene’ key that was referenced in the EndGame scene? Time to add it in. This scene is identical to the EndScene. It just loads a different image and starts ‘Level1’.

src/main.js

import { IntroScene } from ‘./scenes/IntroScene’;
scene: [
  IntroScene,
  Level1,
  Level2,
  EndScene
]

src/scenes/IntroScene.js

import Phaser from ‘phaser’;
export class IntroScene extends Phaser.Scene {
  constructor() {
    super({ key: ‘IntroScene’ });
  }
  preload() {
    this.load.image(‘splash’, ‘assets/images/intro_splash.png’);
  }
  create() {
    // Add the background image
    this.splash = this.add.image(400, 300, ‘splash’);
    this.splash.setInteractive().on(‘pointerdown’, () => {
      this.scene.start(‘Level1’);
    });
  }
}

There you have it! A game ready for the web. This is just a small sample of Phaser’s features. Zeta Bros. is free to experiment with and use as a starting point for your own games. Phaser is very much an indie project, so if you use it in any production capacity consider donating money. Phaser’s creator has a Patreon page and I highly recommend at least getting the $1 a month subscription to the newsletter. It comes with a huge zip archive full of new code samples and example games every month.