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 “transfer” 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* * *

In the next part of this Phaser tutorial, we’ll animate our character. More to come!

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

YOU MAY ALSO LIKE