Phaser.js: A Step-by-Step Tutorial On Making A Phaser 3 Game

In this article, we are going to develop from scratch a game made with Phaser.js. You’ll learn how to set up a build on webpack, load assets, create characters and animations, add keyboard controls, handle a powerful tool for creating maps that is Tiled, and even how to implement a simple bot behavior. Let’s go!

Table of contents

About Phaser

First, a little background. Phaser is an open-source JavaScript 2D game development framework developed by the folks at Photon Storm. It uses Canvas and WebGL renderers. You can play games developed with Phaser 3 in any modern web browser, and with tools like Apache Cordova, you can even turn them into mobile or native desktop apps. Phaser is open-source, easy to get started, and generally a great option for people who are looking to try JS for game development.

What you need to start

  • Basic knowledge of JavaScript
  • Basic knowledge of TypeScript to be able to get what all those “types” are
  • Slight knowledge about webpack
  • A code editor
  • A package manager (yarn OR npm)

That’s it! After completing the tutorial, you’ll be able to create games like this:

Phaser 3 game example

Check it out and play the Demo version.

Here are the assets for this tutorial that you are going to need.

And this is where you can find the final code.

So since it looks like we are ready to start, let’s turn to the initial stage which is about…

Part 1: Installing packages and configuring webpack

We begin by setting up the environment, the required packages, and setting up webpack.

For this tutorial, we’ll be using the yarn package manager, but you can use the npm one as well since we only need it to install packages and launch the app build.

Preparing the file structure

mkdir game-example
cd game-example
mkdir src src/assets src/classes src/scenes

We get the following structure:

├── src
    ├── assets
    ├── classes
    └── scenes

assets — this is where we’ll store all game assets: png sprites, sprite sheets, and JSON files.

classes — for classes (player, score meter, etc.).

scenes — a place to store the game scenes.

Let’s add the index.html file to src/ an make it an entry point of the application:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0">
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
    }
  </style>
</head>

<body>
  <div id="game"></div>
</body>
</html>

Here we tell the game to not scale, remove the screen border padding, and specify the div id="game" element that’ll be the parent block where the game will be rendered.

Initialization and packages

Initializing package.json:

yarn init

Filling our package.json with the Phaser itself:

yarn add phaser

And installing typescript and webpack with the necessary plugins and loaders:

yarn add -D typescript @types/node cross-env webpack webpack-cli webpack-dev-server html-webpack-plugin@5.0.0-alpha.10 clean-webpack-plugin copy-webpack-plugin terser-webpack-plugin ts-loader babel-loader @babel/core @babel/preset-env html-loader css-loader  style-loader json-loader

Note that we install html-webpack-plugin@5.0.0-alpha.10 because at the time of this article’s publication the plugin had an unresolved bug.

Add eslint and match it with typescript and prettier for linting and quick code formatting:

yarn add -D eslint eslint-config-prettier eslint-plugin-prettier prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser

Done! We have installed all the necessary packages, now we can proceed to set up the configs.

Setting up configs

We’ll create the configuration files at the root level of the file structure.

tsconfig.json

{
  "compilerOptions": {
    "sourceMap": true,
    "strict": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "module": "es6",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "target": "es5",
    "allowJs": true,
    "baseUrl": ".",
  },
  "include": [
    "./index.d.ts",
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "./dist/"
  ]
}

You can read more about the contents of tsconfig.json in the documentation.

.eslintrc.js

module.exports = {
  parser: '@typescript-eslint/parser',

  plugins: ['@typescript-eslint'],

  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint',
  ],

  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },

  rules: {
    '@typescript-eslint/camelcase': 0,
    '@typescript-eslint/explicit-function-return-type': 0,
    '@typescript-eslint/explicit-member-accessibility': 0,
    '@typescript-eslint/interface-name-prefix': 0,
    '@typescript-eslint/no-explicit-any': 0,
    '@typescript-eslint/no-object-literal-type-assertion': 0,
    'sort-imports': [
      'warn',
      {
        ignoreCase: true,
        ignoreDeclarationSort: true,
        ignoreMemberSort: false,
        memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
      },
    ],
  },
};

.prettierrs.js

module.exports = {
  printWidth: 100,
  semi: true,
  singleQuote: true,
  tabWidth: 2,
  trailingComma: 'all',
};

Optional, to automatically format files when saving to VSCode.

./vscode/settings.json

{
  "typescript.tsdk": "node_modules/typescript/lib",
  "eslint.autoFixOnSave": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "typescript",
      "autoFix": true
    },
  ],
  "[javascript]": {
    "editor.formatOnSave": false
  },
  "[typescript]": {
    "editor.formatOnSave": false
  },
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
}

Turning standard formatting off to avoid situations where it could get in conflict with eslint formatting.

You can customize rules for eslint, prettier, and vscode to better suit your needs, it won’t affect working with Phaser in any way 😌

webpack.config.js

So that we can build our creation and run the dev-server, we need to first make a config for webpack:

/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

const isProd = process.env.NODE_ENV === 'production';

const babelOptions = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: 'last 2 versions, ie 11',
        modules: false,
      },
    ],
  ],
};

const config = {
  mode: isProd ? 'production' : 'development',
  context: path.resolve(__dirname, './src'),
  entry: './index.ts',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },

  module: {
    rules: [
      {
        test: /\.ts(x)?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: babelOptions,
          },
          {
            loader: 'ts-loader',
          },
        ],
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: 'html-loader',
          },
        ],
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },

  resolve: {
    extensions: ['.ts', '.js'],
  },

  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
        terserOptions: {
          output: {
            comments: false,
          },
        },
      }),
    ],
  },

  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: 'index.html',
      inject: true,
      title: 'Phaser Webpack Template',
      appMountId: 'app',
      filename: 'index.html',
      inlineSource: '.(js|css)$',
      minify: false,
    }),
  ],

  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 5000,
    inline: true,
    hot: true,
    overlay: true,
  },
};

module.exports = config;

What we indicate in the config: 

  • what loaders to use for files
  • what plugins to apply during the build
  • that the minification is done using Terser
  • dev-server settings. 

You can find out more about each field of the webpack.config.js file on the official site.

Done!

We have described all the required configs. Now, all we need is to just write a couple of scripts to start dev-server and run the build.

Let’s go to package.json and add this field:

...
"scripts": {
	"dev": "cross-env NODE_ENV=development webpack serve",
	"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
},
...

Awesome!

Now let’s create a src/index.ts file that acts as an entry point for the bundle scripts and add some code snippets there to make sure everything works fine after starting dev-server.

src/index.ts

console.log('Hello world!');

Add a header to src/index.html just for testing purposes, and run dev-server to make sure everything’s OK.

To start dev-server, execute the command yarn dev.

Phaser - Hello World

“Hello, world!” and the “Yep!” message in the console indicate that everything is set up alright and we can finally move on to Phaser itself 😎

Part 2: The first scene, loading assets and showing a character on screen

Setting up a game

In the previous part, we set up the entire development environment from scratch including webpack, TypeScript, linter, and formatter, which means it’s time to move on to setting up the game and begin placing objects. Don’t worry, it’s not one massive file. In this part, we’ll start small by showing our character.

First, we need to declare a Game object. It’s the most important of the required objects since Phaser won’t initialize without it. We’ll initialize it at the “entry point”, namely src/index.ts.

A Game must contain the second required object — a Scene, at least one. A Scene in the Phaser world is similar to a theater scene in the real world. It contains child elements like a real scene contains actors. These can be Sprites, Images, and Containers. For a start, this set of elements is enough for our purposes, since we’ll be creating our own classes for child elements, inheriting their class properties.

To declare a game, we need to indicate what parameters we’ll launch it with. So, describe the following in the parameters:

  • title — game title
  • type — render type, can be CANVAS, WEBGL, or AUTO. Many effects can be unavailable with the CANVAS type that are available with the WEBGL one. In this tutorial, we’ll be using the latter
  • parent — DOM element id of the page where we’ll add a Game canvas element)
  • backgroundColor — the background color of the canvas
  • scale — setting for resizing the game canvas. Our choice is mode: Phaser.Scale.ScaleModes.NONE since we’ll have our own sizing system. Read more about modes here
  • physics — an object for setting the game physics
  • render — additional properties of a game render
  • callbacks — callbacks that will be triggered BEFORE (preBoot) or AFTER (postBoot) the game is initialized
  • canvasStyle — CSS styles for the canvas element where the game will be rendered
  • autoFocus — autofocus on the game canvas
  • audio — sound system settings
  • scene — a list of scenes to load and use in the game.

To declare a game, we need to indicate what parameters we’ll launch it with. So, describe the following in the parameters:

src/index.ts

import { Game, Types } from 'phaser';

import { LoadingScene } from './scenes';

const gameConfig: Types.Core.GameConfig = {
	title: 'Phaser game tutorial',
  type: Phaser.WEBGL,
  parent: 'game',
  backgroundColor: '#351f1b',
  scale: {
    mode: Phaser.Scale.ScaleModes.NONE,
    width: window.innerWidth,
    height: window.innerHeight,
  },
  physics: {
    default: 'arcade',
    arcade: {
      debug: false,
    },
  },
  render: {
    antialiasGL: false,
    pixelArt: true,
  },
  callbacks: {
    postBoot: () => {
      window.sizeChanged();
    },
  },
  canvasStyle: `display: block; width: 100%; height: 100%;`,
  autoFocus: true,
  audio: {
    disableWebAudio: false,
  },
  scene: [LoadingScene],
};

Let’s add to the same file a global function for resizing our game:

src/index.ts

...

window.sizeChanged = () => {
  if (window.game.isBooted) {
    setTimeout(() => {
      window.game.scale.resize(window.innerWidth, window.innerHeight);

      window.game.canvas.setAttribute(
        'style',
        `display: block; width: ${window.innerWidth}px; height: ${window.innerHeight}px;`,
      );
    }, 100);
  }
};

window.onresize = () => window.sizeChanged();

Finally, we can create a Game itself:

src/index.ts

...

window.game = new Game(gameConfig);

To avoid getting the error about Window not having a window.sizeChanged method and a global window.game object, let’s patch the Window interface:

interface Window {
  sizeChanged: () => void;
  game: Phaser.Game;
}

Now TypeScript understands what is behind window.game and window.sizeChanged.

Scene creation

If we run the game without a single scene, we’ll get an error. Let’s create the first scene then, which will later act as the main scene for loading assets and launching the rest of the scenes.

Create a scene Loading file to describe the Scene Class. In the constructor, indicate the Scene Key. We’ll use it to select a specific scene among others. The Scene Key is a required parameter.

src/scenes/loading/index.ts

import { Scene } from 'phaser';

export class LoadingScene extends Scene {
  constructor() {
    super('loading-scene');
  }

  create(): void {
    console.log('Loading scene was created');
  }
}

As you can see, we are using the create() {...} method. It is one of the built-in scene methods and is the scene lifecycle method. There are several such methods:

  • init(data) {} — kicks in when a scene is created. It accepts the Data Object that we can pass when we call game.scenes.add(dataForInit) or game.scenes.start(dataForInit). For example, when we create a scene while being in some other scene (yes, you can do that). All scenes will be at the same hierarchy level, with no nested scenes.
  • preload() {} — a method that defines what we need to load before the scene and from where. We’ll use it to load assets later on.
  • create(data) {} — a method that gets triggered when a scene is created. In it, we’ll specify positioning for such scene elements as Character and Enemies.
  • update(time, delta) {} — a method that gets called with every render frame (on average, 60 times per second). It’s a game loop in which redrawing, moving objects, etc. occurs.

Also, for convenient scene import, let’s create a file with which we will “transmit” the exports in nested directories.

src/scenes/index.ts

export * from './loading';

Now we can start our game to see that the scene has been created and has triggered the message to the console, which we’d specified in the create() method. The brown of the background is the backgroundColor we specified in gameConfig.

Phaser 3: First scene

Let’s load a character for this background!

To do it, create a folder src/assets/sprites/, and add there a picture of our character —  king.png. Next, in the preload() method of the scene class, add the following:

src/scenes/loading/index.ts

...
preload(): void {
	this.load.baseURL = 'assets/';

	// key: 'king'
	// path from baseURL to file: 'sprites/king.png'
	this.load.image('king', 'sprites/king.png');
}
...

Now to creating our character, let it be a simple sprite. Declare a character field, and write the following in the scene’s create() method:

src/scenes/loading/index.ts

export class LoadingScene extends Scene {
  private king!: GameObjects.Sprite;

	constructor() {...}
	...
	create(): void {
		this.king = this.add.sprite(100, 100, 'king');
	}
...

Here we indicate that we add a sprite (add.sprite), place it at such and such XY coordinates, and use a texture with the 'king' key which we specified during loading in the preload() method.

Now, if we run the dev-server, instead of a character we’ll get a black square with a green border. Squares like these signal that Phaser was unable to detect the texture or sprite along a path.

But the path is right, so what gives?

It has to do with webpack. We haven’t specified the assets’ movement when building or running in dev mode. Let’s fix it.

Add plugin import and initialization to webpack.config.js.

webpack.config.js

...
const CopyWebpackPlugin = require('copy-webpack-plugin');
...
plugins: [
	...,
	new CopyWebpackPlugin({
      patterns: [
        {
          from: 'assets',
          to: 'assets',
        },
      ],
    }),
	...
]

Now, when running in dev mode, we can see our character 😎

Phaser 3 - Show player

Part 3: Animating a character, adding the ability to move, keybinding

Great! We now know how to load and place sprites. One thing left… Need more interactivity! Let’s bring our character to life by giving him the ability to move.

We’ll start by creating a separate class for our character and a separate scene for the level and call it level-1-scene. We begin with the scene since we’ve already placed our character there in the previous part of this tutorial. 

Let’s create a scene folder with the index.ts scene file.

src/scenes/level-1/index.ts

import { Scene } from 'phaser';

export class Level1 extends Scene {
  constructor() {
    super('level-1-scene');
  }

  create(): void {
		...
	}
}

Don’t forget to add the “transmitter” to the scene. 

src/scenes/index.ts 

export * from './loading';
export * from './level-1';

Now we need to add our level-1 scene to the scene array in gameConfig.

src/index.ts

import { Level1, LoadingScene } from './scenes';
...
const gameConfig = {
	...
	scene: [LoadingScene, Level1],
	...
};

And move our king from the Loading scene to the Level1 scene:

import { GameObjects, Scene } from 'phaser';

export class Level1 extends Scene {
  private king!: GameObjects.Sprite;
  
  constructor() {
    super('level-1-scene');
  }

  create(): void {
    this.king = this.add.sprite(100, 100, 'king');
  }
}

In the Loading scene, we set our scene-level to launch after all other loadings are finished:

src/scenes/loading/index.ts

...
preload(): void {
  this.load.baseURL = 'assets/';
  this.load.image('king', 'sprites/king.png');
}
...
create(): void {
  this.scene.start('level-1-scene');
}
...

In the this.scene.start() method, we specify the key of the scene we want to launch.

Let’s run the dev server and make sure our character is displayed.

Awesome! Looks like he’s in his proper place.

Creating an actor

Since in the future there will be enemies behaving somewhat similar to the player character, let’s create an actor class to store general properties and methods, and then create a player class by extending the actor one.

src/classes/actor.ts

import { Physics } from 'phaser';

export class Actor extends Physics.Arcade.Sprite {
	protected hp = 100;

  constructor(scene: Phaser.Scene, x: number, y: number, texture: string, frame?: string | number) {
    super(scene, x, y, texture, frame);

    scene.add.existing(this);
    scene.physics.add.existing(this);

    this.getBody().setCollideWorldBounds(true);
  }

	public getDamage(value?: number): void {
    this.scene.tweens.add({
      targets: this,
      duration: 100,
      repeat: 3,
      yoyo: true,
      alpha: 0.5,
      onStart: () => {
        if (value) {
          this.hp = this.hp - value;
        }
      },
      onComplete: () => {
        this.setAlpha(1);
      },
    });
  }

	public getHPValue(): number {
    return this.hp;
  }

	protected checkFlip(): void {
    if (this.body.velocity.x < 0) {
      this.scaleX = -1;
    } else {
      this.scaleX = 1;
    }
  }

  protected getBody(): Physics.Arcade.Body {
    return this.body as Physics.Arcade.Body;
  }
}

Here we indicate that the actor class is not just some sprite but a physical one. This is to adjust its collisions and physical dimensions for contact with walls and other objects.

The scene.add.existing(this) and scene.physics.add.existing(this) methods tell us that we are adding our physical sprite to the scene and need it considered in terms of scene physics.

The this.getBody().setCollideWorldBounds(true) method tells the world to react to the physical model of the sprite, its “box”.

The protected checkFlip() method is designed to rotate an actor as it moves left or right.

The public getDamage() method is for attacking the actor. We can see a tween inside — something like an animation done by manipulating some properties of the target object. Here we describe that the alpha transparency will change 3 times repeat: 3 within 100 ms duration: 100, each time returning to the original alpha value yoyo: true. At the start of the blinking animation onStart, we change the actor’s HP value in accordance with how much damage it received. At the end of the onComplete animation, we forcefully set the current character’s opacity to this.setAlpha(1). The value of the alpha property varies from 0 to 1. The lower the value, the more transparent the object.

The getBody() method was created due to errors in Phaser types and helps us get that very physical body model of a physical object.

Creating a player character

Now that the main actor class has been created, we can create the player class.

src/classes/player.ts

import { Actor } from './actor';

export class Player extends Actor {
  private keyW: Phaser.Input.Keyboard.Key;
  private keyA: Phaser.Input.Keyboard.Key;
  private keyS: Phaser.Input.Keyboard.Key;
  private keyD: Phaser.Input.Keyboard.Key;

  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'king');

    // KEYS
    this.keyW = this.scene.input.keyboard.addKey('W');
    this.keyA = this.scene.input.keyboard.addKey('A');
    this.keyS = this.scene.input.keyboard.addKey('S');
    this.keyD = this.scene.input.keyboard.addKey('D');

    // PHYSICS
    this.getBody().setSize(30, 30);
    this.getBody().setOffset(8, 0);
  }

  update(): void {
    this.getBody().setVelocity(0);

    if (this.keyW?.isDown) {
      this.body.velocity.y = -110;
    }

    if (this.keyA?.isDown) {
      this.body.velocity.x = -110;
      this.checkFlip();
      this.getBody().setOffset(48, 15);
    }

    if (this.keyS?.isDown) {
      this.body.velocity.y = 110;
    }

    if (this.keyD?.isDown) {
      this.body.velocity.x = 110;
      this.checkFlip();
      this.getBody().setOffset(15, 15);
    }
  }
}

Let’s analyze the code above.

Here we describe the keys to track which one of them is being pressed:

private keyW: Phaser.Input.Keyboard.Key;
private keyA: Phaser.Input.Keyboard.Key;
private keyS: Phaser.Input.Keyboard.Key;
private keyD: Phaser.Input.Keyboard.Key;
...
this.keyW = this.scene.input.keyboard.addKey('W');
this.keyA = this.scene.input.keyboard.addKey('A');
this.keyS = this.scene.input.keyboard.addKey('S');
this.keyD = this.scene.input.keyboard.addKey('D');

In each frame, we check if any control key is pressed and change the XY movement speed depending on the direction. If a key is not pressed, set the speed = 0. Also, when moving left-right, we check whether we need to rotate the character.

update(): void {
    this.getBody().setVelocity(0);

    if (this.keyW?.isDown) {
      this.body.velocity.y = -110;
    }

    if (this.keyA?.isDown) {
      this.body.velocity.x = -110;
      this.checkFlip();
      this.getBody().setOffset(48, 15);
    }

    if (this.keyS?.isDown) {
      this.body.velocity.y = 110;
    }

    if (this.keyD?.isDown) {
      this.body.velocity.x = 110;
      this.checkFlip();
      this.getBody().setOffset(15, 15);
    }
  }

Sometimes character sprites are quite large or have white space. In this case, we can specify the size of the physical model (box) setSize(width, height) and set the point by XY coordinates to calculate the physical model.

this.getBody().setSize(30, 30);
this.getBody().setOffset(8, 0);

Note that when rotating the character, we move the rendering point of the physical model. We have to do it because of the body miscalculation error of Phaser.

Adding a player character

Wonderful! We have created a character class, now we need to create a character on the first level scene.

src/scenes/level-1/index.ts

import { Scene } from 'phaser';

import { Player } from '../../classes/player';

export class Level1 extends Scene {
  private player!: Player;

  constructor() {
    super('level-1-scene');
  }

  create(): void {
    this.player = new Player(this, 100, 100);
  }

  update(): void {
    this.player.update();
  }
}

In the update() method of the scene, we specify for each frame to call the update() method on the player character, which in turn changes its position.

Time for a bit of running around

Run the dev server and try some running around with your character.

Excellent! The character moves and turns in the right direction 😎

Phaser 3: Player movement


Part 4: Sprite sheets and movement animation

OK, our character can move now, but quite clumsily. Let’s add a movement animation and figure out what sprite sheets are.

Sprite sheets and atlases

2D animation in Phaser is created using frames. Frames are sprites. Sprite sheets (atlases) are collections of sprites. The sprites in a sprite sheet are called frames. That is, you can create animation using frames from a sprite sheet.

For example, this is how our character’s sprite looks like:

Phaser 3 character_sprite

And this is the sprite sheet with frames which we’ll use for animation:

Phaser 3 character sprite sheet

But one sprite sheet is not enough. How can we indicate that such and such a frame is in such and such a position of the sprite sheet when a sprite sheet is just a picture? JSON comes to the rescue with information on each sprite: its name, width, height, and XY coordinates on the sprite sheet. Often, sprite sheets are generated automatically using online services or offline solutions, so you don’t have to create JSON yourself and manually lay the sprites out on the grid. Also, some generators add additional parameters to JSON like the anchor property, for example, which indicates the point at the origin of XY coordinates, where 0 is the beginning, 0.5 is the middle, and 1 is the end of the sprite. The anchor value can be negative, and not necessarily compliant with half sizes, that is, the value can be 1.23, or 0.99, or -3.33.

Let’s take a look at the description of a frame:

src/assets/spritesheets/a-king_atlas.json

{
	"frames": [
		{
			"filename": "attack-0",
			"frame": {
				"w": 78,
				"h": 58,
				"x": 0,
				"y": 0
			},
			"anchor": {
				"x": 0.5,
				"y": 0.5
			}
		},
				...
	]
}

Here we can see that the frame called attack-0 is located at 0-0 coordinates and has a width of 78px and a height of 58px.

Downloading the atlas

To use an atlas by its key it must be loaded like the rest of the assets.

Let’s go to the Loading scene and load our atlas and its JSON file.

src/scenes/loading/index.ts

preload(): void {
	this.load.baseURL = 'assets/';

	// Our king texture
	this.load.image('king', 'sprites/king.png');
	// Our king atlas
	this.load.atlas('a-king', 'spritesheets/a-king.png', 'spritesheets/a-king_atlas.json');
}

Now we can use our atlas in the right places with the a-king texture key.

Let’s add running animation to our character! Go to the player class and describe the method for creating animation.

src/classes/player.ts

export class Player extends Actor {
...
	constructor(...) {
		...
		this.initAnimations();
	}
...
	private initAnimations(): void {
    this.scene.anims.create({
      key: 'run',
      frames: this.scene.anims.generateFrameNames('a-king', {
        prefix: 'run-',
        end: 7,
      }),
      frameRate: 8,
    });

    this.scene.anims.create({
      key: 'attack',
      frames: this.scene.anims.generateFrameNames('a-king', {
        prefix: 'attack-',
        end: 2,
      }),
      frameRate: 8,
    });
	}
...

Here we indicate that we want to create an animation with such and such a key with the animation frames taken from the a-king atlas, by the prefix.

Great, we’ve created two animations: run and attack. Now let’s add the running animation:

src/classes/player.ts

...
update(): void {
  this.getBody().setVelocity(0);

  if (this.keyW?.isDown) {
    this.body.velocity.y = -110;
		!this.anims.isPlaying && this.anims.play('run', true);
  }

  if (this.keyA?.isDown) {
    this.body.velocity.x = -110;
    this.checkFlip();
    this.getBody().setOffset(48, 15);
		!this.anims.isPlaying && this.anims.play('run', true);
  }

  if (this.keyS?.isDown) {
    this.body.velocity.y = 110;
		!this.anims.isPlaying && this.anims.play('run', true);
  }

  if (this.keyD?.isDown) {
    this.body.velocity.x = 110;
    this.checkFlip();
    this.getBody().setOffset(15, 15);
		!this.anims.isPlaying && this.anims.play('run', true);
  }
}
...

Here we check for each button whether the animation is currently playing, and if not, we play it with such and such this.anims.play ('key', ignoreIfPlaying) key.

Now let’s start the dev-server and try running😎

Phaser 3 player animation

Part 5: Creating and loading a map, enabling collisions

So, we managed to create a development environment from scratch, load assets, and place them on the stage, and we also added a character with the ability to move. Now let’s make a location for him to be actually able to do that.

A little theory before practice

To create a location, we need the Tiled editor. It’s free and its developers appreciate donations that help them make it bigger and better.

We also need assets — the so-called tileset. They are very similar to sprite sheets but are rigidly attached to the grid. Usually, tileset authors indicate the dimension of a grid. In the assets for this tutorial, we attached the necessary assets (assets/tilemaps/tiles), but you can also find some free packages available in the public domain, for example, on itch.io. Follow the example from the tutorial and create your own world.

Preparing the editor

Open the Tiled editor and choose the New Tileset option.

Phaser.js tutorial: Tiled 1 You’ll see the menu for adding a tileset to a project. Select the tileset from the tutorial materials (assets/tilemaps/tiles/dungeon-16-16). Now you need to specify the tile dimension (grid dimension): Tile width and Tile height. In our case, it’s 16×16. Also, make sure that you check the “Embed in map” box. In case it cannot be installed or removed, it’s okay, we’ll return to it later.

Click OK.

Phaser.js tutorial: Tiled 2

Next, Tiled is going to ask you about the project file name and where to save it. The project file applies only to the Tiled editor, it’s not used in the game, so we can save it wherever we want and under any name. Just don’t forget to select the format of the saved file. We leave it to be the standard .tsx.

What we see before us now is the tile tab, where we can select any tile and edit it like adding any parameters, keys, values, names, etc. For now, let’s leave it as it is, but we are definitely going to return here later.

Phaser.js tutorial: Tiled 3

By adding a tileset — the material for creating the map — we can finally create the map itself!

To do this call the menu File → New → New Map…

We have a choice of map parameters before us. We are going to create a map with standard settings for the width and height (Map size), measured in tiles, and set the sizes of our tiles (Tile size) according to our tileset. As our tileset has a grid of 16×16 pixels, so in this window, we set the Tile size to 16×16 pixels.

Phaser.js tutorial: Tiled 4

Great! We now have a map editing tab:

Phaser.js tutorial: Tiled 5

Let’s analyze the areas:

  • Map Properties — these are map settings that you can edit in case you make a mistake in the New Map… menu.
  • Map editor — it’s the map grid where we’ll place our tiles. That is, it’s like a canvas for drawing a map with tiles🙂
  • Layers — these are map layers. There can be many of them and they can be of different types. For example: on the Ground layer we’ll mark the places where our character will move, and on the Walls layer we’ll place walls and obstacles through which he won’t be able to pass. Splitting into layers is a good practice since it is both more convenient for creating and editing a map, as well as for a more handy selection of the necessary layers for using some events in the game specifically with these layers.
  • Tiles — this is the tileset window of the tileset we loaded earlier.

Creating a location

So let’s create a location already!

To do this, in the layers area, select the Layers tab, then select the standard layer and rename it. In this tutorial, we’ll call it Ground, but you can choose any name.

Now select the tiles from the Tiles area and place them on the map, like putting brush to canvas.

You can use any of the tools from the toolbar above the Map editor. Stamp Brush, Shape Fill Tool, and Eraser seem to be the most popular ones.

Now let’s add another layer with walls and name it Walls.

And here’s our first map!

Phaser.js tutorial: Tiled 6

That’s enough for a start. Let’s transfer the map into the game.

Exporting and loading a map into a game

Save the Tiled project.

Then go to File → Export As…

You’ll see the window for saving the map file. Select the JSON map files format and save. Let’s do it under the project name — dungeon.

Create folders src/assets/tilemaps/tiles/ for tilesets files (.png images) and src/assets/tilemaps/json/ for json map files, where we’ll transfer our map files.

As a result, our src/assets/ folder will look like this:

├── sprites
│   └── king.png
├── spritesheets
│   ├── a-king.png
│   └── a-king_atlas.json
└── tilemaps
    ├── json
    │   └── dungeon.json
    └── tiles
        └── dungeon-16-16.png

Next, load the map using Phaser. To do this, go to the Loading scene, where we load all the assets, and add the following:

this.load.image({
  key: 'tiles',
  url: 'tilemaps/tiles/dungeon-16-16.png',
});
this.load.tilemapTiledJSON('dungeon', 'tilemaps/json/dungeon.json');

So, first, we load the texture, our tileset, and then load the JSON map file, which stores information about the location of each tile and the way it’s divided into layers.

Go to the scene of the first level, and define our map to display it.

Let’s create a function for initializing the map, and describe the property types:

private map!: Tilemaps.Tilemap;
private tileset!: Tilemaps.Tileset;
private wallsLayer!: Tilemaps.DynamicTilemapLayer;
private groundLayer!: Tilemaps.DynamicTilemapLayer;
...
private initMap(): void {
  this.map = this.make.tilemap({ key: 'dungeon', tileWidth: 16, tileHeight: 16 });
  this.tileset = this.map.addTilesetImage('dungeon', 'tiles');
  this.groundLayer = this.map.createDynamicLayer('Ground', this.tileset, 0, 0);
  this.wallsLayer = this.map.createDynamicLayer('Walls', this.tileset, 0, 0);

  this.physics.world.setBounds(0, 0, this.wallsLayer.width, this.wallsLayer.height);
}

Here, we define tilemap this.map with so-and-so a key and such-and-such width and height tile parameters. After that, we define the connection between this.tileset of the texture and the JSON file of the map (we indicate the keys that we used while loading on the Loading scene). Next, describe the this.groundLayer and this.wallsLayes layers where the first argument is the name of the layer, the second one is the map information, i.e. this.tileset, and the arguments number 3 and 4 are the XY coordinates relative to the world where we start drawing the map. After describing all the information about the map and layers, we set the size of the physical world to this.physics.world.setBounds(), where we state that we start counting at so-and-so XY coordinates, and set the width and height of the world to the similar to the wall layer.

It only remains to call our function at the very beginning of the create() method of the first level scene:

...
create(): void {
  this.initMap();

  this.player = new Player(this, 100, 100);
}
...

Let’s start the dev-server to see that… Our location isn’t there, and in the console, we see a warning from Phaser that the tileset “dungeon” could not be loaded. Don’t worry, everything is just as it should be!

This is because when loading the tileset into Tiled, we didn’t change its name. And if you did everything according to the tutorial, now the map inside the JSON code is trying to associate itself with a tileset named “dungeon-16-16”. Let’s fix it!

In the tilesets area, click on the settings icon for this tileset, and in the window that opens, change the Name property from “dungeon-16-16” to “dungeon”. Save.

Phaser.js tutorial: Tiled 7 Phaser.js tutorial: Tiled 8

Also, make sure that in the tileset area and with the dungeon selected, the “Embed Tileset” button is not active. If it is active, click on it.

Phaser.js tutorial: Tiled 9

Now let’s re-export our JSON file. File -> Export As … -> File format .json.

Transfer the new JSON file into the project and start the dev-server for verification.

Voila! We see the map!

Phaser.js tutorial: Tiled 10

Collisions and walls

Let’s try running around the map. As you can see, the character does not react to the walls, as if the whole map is just a background picture. To fix this, we need to set collisions on certain tiles.

It’s very convenient to do everything there in the Tiled editor.

Open up our “dungeon” tileset and select all the tiles. Further, on the left, there are Properties. In the “Custom Properties” area, right-click → Add Property → choose any property name (we going to use “collides”), the data type is bool → OK.

Phaser.js tutorial: Tiled 11

There should appear a new field with the option to check the box. It might happen so that the created field is of the wrong type. In such a case, just delete it.

Now that all the tiles have the collides property, select only those tiles that cannot be passed through, and check the collides checkbox.

Phaser.js tutorial: Tiled 12

Save the project, export the dungeon.json, and transfer it into the game assets.

Now, go to the scene of the first level and to the map creation function and enable collisions on tiles that we want to select by property:

...
this.wallsLayer.setCollisionByProperty({ collides: true });
...

To see if collisions have been applied OK to the walls, let’s write a short helper function that will simply show us debug information and highlight the collision walls.

...
private initMap(): void {
	...
	this.showDebugWalls();
}
...
private showDebugWalls(): void {
  const debugGraphics = this.add.graphics().setAlpha(0.7);
  this.wallsLayer.renderDebug(debugGraphics, {
    tileColor: null,
    collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255),
  });
}

Start the dev-server and you’ll now see exactly which walls have collisions.

Phaser.js tutorial: Tiled 13

But wait! Our character is still walking through the walls, what’s the matter?

The fact is, we didn’t indicate that the character should react to the walls in some way. Let’s fix this!

On the first level stage, in the create() method, after initializing the map and the player, add the so-called “collider”, a method that includes collisions between two objects:

...
create(): void {
  this.initMap();
  this.player = new Player(this, 100, 100);

  this.physics.add.collider(this.player, this.wallsLayer);
}
...

That’s it, our character is no longer able to pass through the walls:

Phaser.js tutorial: Collision

Now you can create your own locations, load them into the game, and identify impassable areas.

Try to improve your location, or expand it. Try different tiles or even choose a tileset for your own idea, the world is yours😎

Part 6: Adding objects to the map. Сamera

With the help of Tiled, we can not only build a map but also add points to show where to place the objects.

How about rewarding our character with chests? Let’s create it all in the same old Tiled.

Putting object points on the map

To place objects in Tiled, we need to first create a separate layer for them. Go to the Layers area → Right-click on the empty space → New → Object Layer. Let’s call it “Chests”.

Now select “Insert Point” on the toolbar and place points where you want the objects to be.

Select all the points, and on the left side, in the object properties, set the “Name” property to “ChestPoint”.

Phaser.js tutorial: Chest 1

Export the JSON and go to the first level scene.

Placing chests on points

Now we can place any object on any point. In our case, it’s chests.

Due to errors with Phaser types, it’s better to prepare a small helper adapter in src/helpers/gameobject-to-object-point.ts, with which we’ll translate the GameObject type to ObjectPoint.

export const gameObjectsToObjectPoints = (gameObjects: unknown[]): ObjectPoint[] => {
  return gameObjects.map(gameObject => gameObject as ObjectPoint);
};

Let’s define the ObjectPoint type in index.d.ts:

type ObjectPoint = {
  height: number;
  id: number;
  name: string;
  point: boolean;
  rotation: number;
  type: string;
  visible: boolean;
  width: number;
  x: number;
  y: number;
};

Also, we’ll need to separately load our tileset as a sprite sheet so that we can take certain sprites from there (note, the sprites, not the tiles). To do this, go to the Loading scene and add the loading of the sprite sheet with the tiles_spr key (the name can be anything), specifying the dimension of the sprites:

...
this.load.spritesheet('tiles_spr', 'tilemaps/tiles/dungeon-16-16.png', {
  frameWidth: 16,
  frameHeight: 16,
});
...

Now, on the first level stage, we’ll define an array of chests, add a function that creates chests and call it after initialization of the character:

private chests!: Phaser.GameObjects.Sprite[];
...

...
private initChests(): void {
  const chestPoints = gameObjectsToObjectPoints(
    this.map.filterObjects('Chests', obj => obj.name === 'ChestPoint'),
  );

  this.chests = chestPoints.map(chestPoint =>
    this.physics.add.sprite(chestPoint.x, chestPoint.y, 'tiles_spr', 595).setScale(1.5),
  );

  this.chests.forEach(chest => {
    this.physics.add.overlap(this.player, chest, (obj1, obj2) => {
      obj2.destroy();
      this.cameras.main.flash();
    });
  });
}

Using the this.map.filterObjects() function, select the required objects from the required layer. The first argument is the layer’s name, the second one is the callback function for filtering. In our case, if the object has the name “ChestPoint”, then it’s suitable, and will fall into the array of the same chestPoints objects.

Next, let’s create a sprite with a physical model for each point from the chestPoints array.

595 is the sprite’s ID in the sprite sheet, but how to find it? It’s simple! Open the tileset in the Tiled editor, select the chest, and on the left side, you’ll see its ID:

Phaser.js tutorial: Chest 2

Also, we made sure that when a character “steps” on a chest, there will be a flash, and the chest will disappear as if it had been taken.

Let’s start the dev server and check it:

Phaser.js tutorial: Chest loot

Cameras and following the player character

Cool, we can now collect chests! But what to do with those chests that are on the map, but that we don’t see? Time to work with the map.

Each scene has a main camera already, and this is how it is being accessed — through this.cameras.main. Also, we can create our own cameras. For this tutorial, the main camera is enough, but if you want to learn more about cameras, be sure to look into these examples.

Let’s add a function, where we’ll call several methods for the camera and call the function after all initializations in the create() method:

private initCamera(): void {
  this.cameras.main.setSize(this.game.scale.width, this.game.scale.height);
  this.cameras.main.startFollow(this.player, true, 0.09, 0.09);
  this.cameras.main.setZoom(2);
}

Here we have set the camera size, zoom, and the target that the camera will follow. 0.09 is the value for the smooth movement of the camera towards the target position, thus we made the movement of the camera smoother.

Phaser.js tutorial: Camera movement

* * *

Next time we’ll cover the subjects of text, event system, and counter. Stay tuned!

A battle-ready Development Team
Building for crazy startups and reputable businesses.
Written by Denis Kratos
May 05, 2021

YOU MAY ALSO LIKE