• How to build a simple game in the browser with Phaser 3 and TypeScript
  • Mariya Davydova
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: iceytea
  • Proofreader: WZnonstop, BYChoo

Photo by Phil Botha and posted on Unsplash

I’m a back-end developer, and my front-end expertise is relatively weak. A while ago I wanted to have some fun — making games in a browser; I chose the Phaser 3 framework (which seems very popular right now) and the TypeScript language (because I prefer statically typed languages to dynamically typed ones). It turns out you need to do some boring things to make it work properly, so I wrote this tutorial to help others like me get started faster.

Preparing the development environment

IDE

Choose your development environment. You can always use plain old Notepad if you want, but I recommend you use a more helpful IDE. As for me, I prefer to develop my best projects in Emacs, so I installed Tide and followed the instructions.

Node

If we were developing in JavaScript, we could start coding without these preparatory steps. However, because we want to use TypeScript, we have to set up the infrastructure for future development as quickly as possible. So we need to install Node and NPM.

When I wrote this tutorial, I was using Node 10.13.0 and NPM 6.4.1. Note that versions in the front-end world update very quickly, so you only need to use the latest stable version. I strongly recommend that you use NVM instead of manually installing Node and NPM, which will save you a lot of time and effort.

Set up the project

The project structure

We will use NPM to build the project, so to start the project, go to an empty folder and run NPM init. NPM will ask you a few questions about the project properties and then create a package.json file. It looks something like this:

{
  "name": "Starfall"."version": "0.1.0 from"."description": "Starfall game (Phaser 3 + TypeScript)"."main": "index.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Mariya Davydova"."license": "MIT"
}
Copy the code

The software package

Use the following command to install the package we need:

npm install -D typescript webpack webpack-cli ts-loader phaser live-server
Copy the code

The -d option (full –save-dev) causes NPM to automatically add these packages to the devDependencies list in package.json:

"DevDependencies" : {" live - server ":" ^ 1.2.1 ", "phaser" : "^ 3.15.1", "ts - loader" : "^ 5.3.0", "typescript" : "^ 3.1.6 webpack", ""," ^ 4.26.0 ", "webpack - cli" : "^ 3.1.2"}Copy the code

Webpack

Webpack will run the TypeScript compiler and collect a bunch of generated JS files and libraries into a compressed JS that we can include in our pages.

Add webpack.config.js near package.json:

const path = require('path');

module.exports = {
  entry: './src/app.ts'.module: {
    rules: [{test: /\.tsx? $/.use: 'ts-loader'.exclude: /node_modules/}},resolve: {
    extensions: [ '.ts'.'.tsx'.'.js']},output: {
    filename: 'app.js'.path: path.resolve(__dirname, 'dist')},mode: 'development'
};
Copy the code

Here we see that Webpack has to start getting the source code from SRC /app.ts (which we’ll add soon) and collect everything in the dist/app.js file.

TypeScript

We also need a small configuration for the TypeScript compiler (tsconfig.json) that describes which JS version we want to compile the source code into and where to find it:

{
  "compilerOptions": {
    "target": "es5"
  },
  "include": [
    "src/*"]}Copy the code

TypeScript definition

TypeScript is a statically typed language. Therefore, it requires a compiled type definition (.d.ts). At the time of writing this tutorial, the Phaser 3 definitions are not yet available as NPM packages, so you may need to download them from the official repository and place the files in the SRC subdirectory of your project.

Scripts

We’ve almost finished setting up the project. At this point you should create package.json, webpack.config.js and tsconfig.json and add SRC /phaser.d.ts. The last thing we need to do before we start writing the code is to explain how NPM relates to the project. We update the scripts section of package.json as follows:

"scripts": {
  "build": "webpack"."start": "webpack --watch & live-server --port=8085"
}
Copy the code

When NPM build is executed, WebPack builds the app.js file based on the configuration. When you run NPM start, you don’t have to bother building the process, webPack will rebuild the application as soon as you save any updates; Live-server will reload it in the default browser. The application will be hosted at http://127.0.0.1:8085/.

An introduction to

Now that we have the infrastructure in place (a part of the project that I hate when starting a project), we can finally start coding. In this step, we will do a simple thing: draw a dark blue rectangle in the browser window. Using a large game development framework is a bit… HMM… It’s too much. However, we will use it in the next steps.

Let me briefly explain the main concepts of Phaser 3. A Game is an instance of the Phaser.Game class (or its descendants). Each Game contains one or more instances of the descendants of phaser. Game. Each scene contains several objects (static or dynamic) that represent the logical parts of the game. For example, our trivial game will have three scenes: the welcome screen, the game itself, and the score screen.

Let’s start coding.

First, create a simple HTML container for your game. Create an index.html file that contains the following code:


      
<html>
  <head>
    <title>Starfall</title>
    <script src="dist/app.js"></script>
  </head>
  <body>
    <div id="game"></div>
  </body>
</html>
Copy the code

There are only two basic parts: the first is the script tag, which indicates that we will use the file we built here; The second is the div tag, which will be the game container.

Now create the SRC /app.ts file and add the following code:

import "phaser";

const config: GameConfig = {
  title: "Starfall",
  width: 800,
  height: 600,
  parent: "game"
  backgroundColor: "#18216D"
};

export class StarfallGame extends Phaser.Game {
  constructor(config: GameConfig) {
    super(config); }}window.onload = (a)= > {
  var game = new StarfallGame(config);
};
Copy the code

This code is self-explanatory. GameConfig has a number of different properties, which you can check out here.

Now you can finally run NPM start. If you’ve done everything in this and the previous steps, you should see something simple in your browser:

Let the stars fall

We created a basic application. Now it’s time to add a scenario where something happens. Our game was simple: stars would fall to the ground, and the goal was to capture as many stars as possible.

To do this, create a new file, gamescene.ts, and add the following code:

import "phaser";

export class GameScene extends Phaser.Scene {

  constructor() {
    super({
      key: "GameScene"
    });
  }

  init(params): void {
    // TODO
  }

  preload(): void {
    // TODO
  }
  
  create(): void {
    // TODO
  }

  update(time): void {
    // TODO}};Copy the code

The constructor here contains a key under which other scenarios can call the scenario.

You see four ways of staking here. Let me briefly explain the differences:

  • Init ([params]) is called at the beginning of the scenario. This function accepts arguments passed from other scenes or games by calling scene.start(key, [params]).

  • Preload () is called before the scene object is created and contains load resources; These resources will be cached, so they will not be reloaded when the scenario is restarted.

  • Create () is called when a resource is loaded, and usually involves the creation of the main game objects (background, player, obstacles, enemies, etc.).

  • Update ([time]) is called in each tick and contains all the content of the dynamic part of the scene (move, blink, etc.).

To make sure we don’t forget this later, let’s quickly add the following line to game.ts:

import "phaser";
import { GameScene } from "./gameScene";

const config: GameConfig = {
  title: "Starfall",
  width: 800,
  height: 600,
  parent: "game",
  scene: [GameScene],
  physics: {
    default: "arcade",
    arcade: {
      debug: false
    }
  },
  backgroundColor: "# 000033"}; .Copy the code

Our game now knows the game scene. If the game configuration contains a list of scenarios, then the first scenario starts when the game starts. All other scenarios are created, but do not begin until explicitly invoked.

We’ve also added Arcade Physics here (a physics model, here are some examples), which we’ll use to make our stars fall.

Now we can put the content on the skeleton of our game scene.

First, we declare some necessary properties and objects:

export class GameScene extends Phaser.Scene {
  delta: number;
  lastStarTime: number;
  starsCaught: number;
  starsFallen: number; sand: Phaser.Physics.Arcade.StaticGroup; info: Phaser.GameObjects.Text; .Copy the code

Then, we initialize the number:

  init(/*params: any*/) :void {
      this.delta = 1000;
      this.lastStarTime = 0;
      this.starsCaught = 0;
      this.starsFallen = 0;
  }
Copy the code

Now, let’s load a few images:

  preload(): void {
    this.load.setBaseURL(
        "https://raw.githubusercontent.com/mariyadavydova/" +
        "starfall-phaser3-typescript/master/");
    this.load.image("star"."assets/star.png");
    this.load.image("sand"."assets/sand.jpg");
  }
Copy the code

After that, we can prepare our static components. We will create the Earth component where the stars will fall and the text notifies us of our current score:

  create(): void {
    this.sand = this.physics.add.staticGroup({
      key: 'sand',
      frameQuantity: 20
    });
    Phaser.Actions.PlaceOnLine(this.sand.getChildren(),
      new Phaser.Geom.Line(20.580.820.580));
    this.sand.refresh();

    this.info = this.add.text(10.10.' ',
      { font: '24px Arial Bold', fill: '#FBFBAC' });
  }
Copy the code

A group in Phaser 3 is a way to create a set of objects that you want to control together. There are two types of objects: static and dynamic. As you might guess, static objects (floors, walls, various obstacles) don’t move, and dynamic objects (Mario, ships, missiles) do.

We created a static ground group. The pieces were placed along the line. Notice that the line is divided into 20 equal parts (not 19 as you might expect), and that the floor tiles are in each part of the left end, and the tile center is at that point (I hope this gives you an idea of what those numbers mean). We also have to call Refresh () to update the group bounding box, or else conflicts will be checked against the default location (the upper-left corner of the scene).

If you look at the application now in your browser, you should see something like this:

We’ve finally reached the most dynamic part of the scene, the update() function, where the stars fall. This function is called once in 60ms. We hope to send out a new meteor every second. We don’t use dynamic groups for this because each star has a short life cycle: it will be destroyed by a user click or collision with the ground. So, in the emitStar() function, we create a new star and add two event handlers: onClick() and onCollision().

update(time: number) :void {
    var diff: number = time - this.lastStarTime;
    if (diff > this.delta) {
      this.lastStarTime = time;
      if (this.delta > 500) {
        this.delta -= 20;
      }
      this.emitStar();
    }
    this.info.text =
      this.starsCaught + " caught - " +
      this.starsFallen + " fallen (max 3)";
  }

private onClick(star: Phaser.Physics.Arcade.Image): (a)= > void {
    return function () {
      star.setTint(0x00ff00);
      star.setVelocity(0.0);
      this.starsCaught += 1;
      this.time.delayedCall(100.function (star) {
        star.destroy();
      }, [star], this); }}private onFall(star: Phaser.Physics.Arcade.Image): (a)= > void {
    return function () {
      star.setTint(0xff0000);
      this.starsFallen += 1;
      this.time.delayedCall(100.function (star) {
        star.destroy();
      }, [star], this); }}private emitStar(): void {
    var star: Phaser.Physics.Arcade.Image;
    var x = Phaser.Math.Between(25.775);
    var y = 26;
    star = this.physics.add.image(x, y, "star");

star.setDisplaySize(50.50);
    star.setVelocity(0.200);
    star.setInteractive();

star.on('pointerdown'.this.onClick(star), this);
    this.physics.add.collider(star, this.sand, 
      this.onFall(star), null.this);
  }
Copy the code

Finally, we have a game! But it does not yet have victory conditions. We will add it at the end of the tutorial.

Wrap it all up

Usually, a game consists of several scenes. Even if the game is simple, you need an opening scene (with at least a Play button) and an ending scene (showing the outcome of the game session, such as the score or highest level achieved). Let’s add these scenarios to our application.

In our case, they’ll be very similar, because I don’t want to focus too much on the game’s graphic design. After all, this is a programming tutorial.

The welcome scenario will contain the following code in Welcomescene.ts. Notice that when the user clicks on a location in this scene, the game scene is displayed.

import "phaser";

export class WelcomeScene extends Phaser.Scene {
  title: Phaser.GameObjects.Text;
  hint: Phaser.GameObjects.Text;

constructor() {
    super({
      key: "WelcomeScene"
    });
  }

create(): void {
    var titleText: string = "Starfall";
    this.title = this.add.text(150.200, titleText,
      { font: '128px Arial Bold', fill: '#FBFBAC' });

var hintText: string = "Click to start";
    this.hint = this.add.text(300.350, hintText,
      { font: '24px Arial Bold', fill: '#FBFBAC' });

this.input.on('pointerdown'.function (/*pointer*/) {
      this.scene.start("GameScene");
    }, this); }};Copy the code

The scoring scene looks almost identical, and clicking (scorescene.ts) leads to the welcome scene.

import "phaser";

export class ScoreScene extends Phaser.Scene {
  score: number;
  result: Phaser.GameObjects.Text;
  hint: Phaser.GameObjects.Text;

constructor() {
    super({
      key: "ScoreScene"
    });
  }

init(params: any) :void {
    this.score = params.starsCaught;
  }

create(): void {
    var resultText: string = 'Your score is ' + this.score + '! ';
    this.result = this.add.text(200.250, resultText,
      { font: '48px Arial Bold', fill: '#FBFBAC' });

var hintText: string = "Click to restart";
    this.hint = this.add.text(300.350, hintText,
      { font: '24px Arial Bold', fill: '#FBFBAC' });

this.input.on('pointerdown'.function (/*pointer*/) {
      this.scene.start("WelcomeScene");
    }, this); }};Copy the code

We now need to update our main application file: add these scenarios and make WelcomeScene the first on the list:

import "phaser";
import { WelcomeScene } from "./welcomeScene";
import { GameScene } from "./gameScene";
import { ScoreScene } from "./scoreScene";

const config: GameConfig = {
  ...
  scene: [WelcomeScene, GameScene, ScoreScene],
  ...
Copy the code

Did you find anything missing? Yes, we haven’t called ScoreScene! From anywhere yet. When the player misses the third star (the game ends), we call it:

private onFall(star: Phaser.Physics.Arcade.Image): (a)= > void {
    return function () {
      star.setTint(0xff0000);
      this.starsFallen += 1;
      this.time.delayedCall(100.function (star) {
        star.destroy();
        if (this.starsFallen > 2) {
          this.scene.start("ScoreScene", 
            { starsCaught: this.starsCaught });
        }
      }, [star], this); }}Copy the code

Finally, our Starfall game looks like a real game — it can start, end, and even have a leaderboard of scores (how many stars can you capture?). .

I hope this tutorial is as useful to you as it was when I wrote it 😀 and any feedback is greatly appreciated!

You can find the source code for this tutorial here.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.