I am participating in the team competition of Nuggets Community Game Creative Submission Contest. For details, please see: Game Creative Submission Contest

preface

In this article, I will develop an H5 game from scratch, primarily using Phaser3 to make the game. Considering the severe situation of the current epidemic, I also integrated some elements into this game. Meanwhile, I hope the epidemic will end as soon as possible, and I can take off masks as soon as possible, so that WE can see the smiles on each other’s faces.

  • Element 1: Wear a mask when you go out
  • Element two: struggle for life, is to collect food
  • Element 3: Brave pingbo, to kill the demon monster, fight against various dark forces

I don’t think it’s much fun, because I didn’t design too many levels, but this article is definitely a good tutorial. By reading this article, you can master the whole process of H5 game development, so that you can quickly develop a similar RPC game. Let’s see the effect first!

Demo address: game.runjs.cool/

Code repository: github.com/maqi1520/ph…

Using the Technology stack

  • Phaser: Game engine
  • Vite: Project scaffolding for quick startup of web development servers for quick hot updates
  • Typescript: Ts provides powerful type hints that reduce the need to check API documentation

Phaser profile

Phaser is an open source JavaScript 2D game development framework. It uses Canvas and WebGL to render our game, but we don’t have to use Canvas and WebGL apis directly. It encapsulates a large number of game development classes and methods, making it very easy to get started and a good choice for those who want to use JS to develop their game.

Initialization engineering

yarn create vite@latest game-phaser3 --template vanilla-ts

yarn add phaser

cd game-phaser3

mkdir public/assets src src/classes src/scenes
Copy the code

Create a native typescript template using Vite and install phaser,

  • assets— Used to store game materials. For game materials, we can find them on game sharing websites, such as:itch.ioDownload above.
  • classes— Separate classes for game characters, monsters, etc
  • scenes– Used to store the game scene

Initialize the game

Next we need to initialize a gameobject in SRC /index.ts

import { Game, Types, WEBGL } from "phaser";
import { LoadingScene } from "./scenes";

export const gameConfig: Types.Core.GameConfig = {
  type: WEBGL,
  parent: "app".backgroundColor: "#9bd4c3".scale: {
    mode: Scale.ScaleModes.NONE,
    width: window.innerWidth,
    height: window.innerHeight,
  },
  physics: {
    default: "arcade".arcade: {
      debug: false,}},render: {
    antialiasGL: false.pixelArt: true,},callbacks: {
    postBoot: () = >{ sizeChanged(); }},canvasStyle: `display: block; width: 100%; height: 100%; `.autoFocus: true.audio: {
    disableWebAudio: false,},scene: [LoadingScene, GameScene, UIScene],
};

window.game = new Game(gameConfig);
Copy the code
  • Type: The game render type, which can be CANVAS, WEBGL or AUTO. Many effects may not be supported in CANVAS mode, so we use WEBGL

  • Parent: The parent DOM ID of the game rendering canvas element

  • BackgroundColor: backgroundColor of the canvas

  • Scale: Adjusts the scale of the game canvas.

  • Physics: Set up the game physics engine

  • Render: An additional attribute to the game render

  • Callbacks: Callbacks that will be triggered either before (preBoot) or after (postBoot) game initialization

  • CanvasStyle: CSS style of the Canvas element

  • AutoFocus: autoFocus on the game canvas

  • Audio: Game audio Settings

  • Scene: List of scenes to load in the game.

Windows does not have a game object. You need to extend the Window object in viet-env.d.ts

interface Window {
  game: Phaser.Game;
}
Copy the code

Add a method that lets the browser adapt when zooming

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

      window.game.canvas.setAttribute(
        "style".`display: block; width: The ${window.innerWidth}px; height: The ${window.innerHeight}px; `
      );
    }, 100); }}window.onresize = () = > sizeChanged();
Copy the code

Create a new scene

A game is composed of many scenes, and at least one scene is added to a game, usually divided into three scenes: Loading, game and UI

  • Loading scenarios are used to load game resources
  • The game scene is the main part of the game and can be divided into several
  • UI scenarios are used for page UI elements, text prompts, and so on

The following code is an example of a simple scenario

import { Scene } from 'phaser';
export class LoadingScene extends Scene {
  constructor() {
    super('loading-scene');
  }
  init(data) {}
  preload() {
    this.load.baseURL = "assets/";
    this.load.image("king"."sprites/king.png");
  }
  create(data): void {
    this.add.sprite(100.100."king");

  }
  update(time, delta){}}Copy the code

Scenarios also have lifecycle functions

  • Init: the scenario is initialized
  • Preload: What resources need to be loaded before the scene loads
  • Create: Triggered when the scene is created
  • Update: The scene is triggered every render frame is updated (approximately 60 frames per second)

Run Yarn Dev to start. At this point, you should see something like this in your browser

Create the role

Now that the scene is set up, it’s time for the hero to step out. Create the SRC /classes/player.ts file

import { Physics } from "phaser";

export class Player extends Physics.Arcade.Sprite {
  private cursors: Phaser.Types.Input.Keyboard.CursorKeys;
  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, "king");
    scene.add.existing(this);
    scene.physics.add.existing(this);
    this.body.setSize(30.30);
    this.body.setOffset(8.0);
    this.cursors = this.scene.input.keyboard.createCursorKeys();
  }

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

    if (this.cursors.up.isDown) {
      this.body.velocity.y = -110;
    }

    if (this.cursors.left.isDown) {
      this.body.velocity.x = -110;
      this.checkFlip();
      this.setOffset(48.15);
    }

    if (this.cursors.down.isDown) {
      this.body.velocity.y = 110;
    }

    if (this.cursors.right.isDown) {
      this.body.velocity.x = 110;
      this.checkFlip();
      this.setOffset(15.15); }}}Copy the code

Keyboard events

Player inheritance Physics. Arcade. Sprite class, in the instantiation incoming coordinates x, y, and resource ID, by enclosing scene. The input. The rid_device_info_keyboard. CreateCursorKeys keyboard direction key, when the direction key is pressed, Change the speed in Player X and y.

Add a scene game SRC /scenes/game.ts

import { Scene } from "phaser";
import { Player } from ".. /.. /classes/Player";

export class GameScene extends Scene {
  privateplayer! : Player;constructor() {
    super("game-scene");
  }

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

  update(): void {
    this.player.update(); }}Copy the code

Initialize a hero PLayer by calling the update method of the PLayer in the update function.

Then change the create method in loading scenarios to transition from loading scenarios to game scenarios.

create(): void {
   this.scene.start("game-scene");
}
Copy the code

At this point, we can control the hero through the keyboard orientation.

Use Tiled to draw tile map

Next is the map, we need to download Tiled (free) first, to create the game map

Create a new project first, and the gallery layer must select CSV, otherwise Phaser3 will not parse.

Next, build the map block set. Note that you must select the embedded map, otherwise it will not be able to parse.

Tiled is divided into properties area, layer area and image block area. You can select the image block by Commond +A, and then design the game map freely with the stamp tool and rectangle tool.

To prevent the character from moving outside the map, divide the layers into Ground and Walls.

To keep moving objects like characters and monsters from leaving the map, we need to edit the block properties.

Set custom properties on some graph blockscollidestrueLater code can turn on collision detection with this property.

Added anchor points for monsters and food

Right click on the new object layer and rename it to Enimes to add some anchor points. These anchor points can be rendered as monster points in the game, and also need to add some food points.

Select the object layer, the anchor can change the name, according to the name, we can render different objects.

The last step is to export the file as JSON to our assets folder. File -> Export as… -> format.json.

Load tile map

Now that the map is designed, we need to render our map in the game.

Resources are first loaded in the preload method in the loading scenario.

this.load.image({
      key: "Grass".url: "tilemaps/json/Grass.png"});this.load.tilemapTiledJSON("tilemapGrass"."tilemaps/json/Grass.json");

this.load.spritesheet("water"."spritesheets/Water.png", {
  frameWidth: 64.frameHeight: 16});Copy the code

Then add an initMap method to the game to initialize the map

private initMap(): void {
   // Add water as background
   this.add.tileSprite(0.0.window.innerWidth, window.innerHeight, "water");
   this.map = this.make.tilemap({
     key: "tilemapGrass".tileWidth: 16.tileHeight: 16});this.tileset = this.map.addTilesetImage("Grass"."Grass");// The first parameter is the name of the image block, and the second parameter is the key of the image
   this.groundLayer = this.map.createLayer("Ground".this.tileset, 0.0);
   this.wallsLayer = this.map.createLayer("Walls".this.tileset, 0.0);
   // Set the wall with the map block property and enable the collision property
   this.wallsLayer.setCollisionByProperty({ collides: true });
   // Set the edges of the world
   this.physics.world.setBounds(
     0.0.this.wallsLayer.width,
     this.wallsLayer.height
   );
 }
Copy the code

Note that the first name of addTilesetImage must be the same as the design-time block name.

The map is then initialized in the Create method.

create(): void {
  this.initMap();
  this.player = new Player(this.100.100);
}
Copy the code

In Phaser, functions are also executed sequentially, with the method executed first rendering first, at the bottom. So you load the map first, and then you initialize the Player object.

Now you can see a hero in the game scene.

Collision detection

But moving the character, the character walks into the water, so we need to enable collision detection,

In the create method, add the following code to enable collision detection so that the hero cannot step out of the water through the keyboard.

this.physics.add.collider(this.player, this.wallsLayer);
Copy the code

To prevent blocks from having collides attributes set while designing the map, we can highlight the walls that we are colliding so that we can debug them easily.

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(24.234.48.255)}); }Copy the code

The effect is as follows:

Create frame-by-frame animations using Sprite diagrams

Currently our hero is static, to make our hero move when running, we can use the Sprite map, first look at our Sprite map, specially added a mask to the Sprite map.

We also need to load a JSON that describes the Sprite diagram. Let’s take a look at the JSON data structure

JSON describes the position and center point of each frame of the Sprite. Of course, the JSON is not handwritten, so you can use the Texture Packer tool to package it.

Load the Sprite and JSON in Preload

...this.load.atlas(
      "a-king"."spritesheets/a-king_withmask.png"."spritesheets/a-king_atlas.json"); ...Copy the code

Then load the initialization animation in player.js

constructor(scene: Phaser.Scene, x: number, y: number){...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}); }Copy the code

Finally, in the update function, the animation is called when the arrow key is pressed.

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

    if (this.cursors.up.isDown) {
      this.body.velocity.y = -110;
      !this.anims.isPlaying && this.anims.play("run".true); }...if (this.cursors.space.isDown) {
      this.anims.play("attack".true); }}Copy the code

The animation of the character is successful!

Create a monster

As with Player, we can create the same class called Enemy to write monster logic. You just need to load different Sprite resources. Another difference is that the monster’s movements are not controlled by the keyboard, but automatically. So we need to implement the logic of monsters running automatically.

Monster automatic movement mainly has the following two points:

  • Monsters move around in place when they don’t see a character.
  • When the hero is found, the monster will chase the hero. Its principle is to judge the distance between the monster and the player. If the distance is less than a certain value, the monster will set the movement speed.
enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT,
}
// Randomly generate different directions
const randomDirection = (exclude: Direction) = > {
  let newDirection = Phaser.Math.Between(0.3);
  while (newDirection === exclude) {
    newDirection = Phaser.Math.Between(0.3);
  }

  returnnewDirection; }; .// Change the direction of the monster every 2 seconds
this.moveEvent = this.scene.time.addEvent({
      delay: 2000.callback: () = > {
        this.direction = randomDirection(this.direction);
      },
      loop: true}); .Copy the code
private AGRESSOR_RADIUS = 100;

preUpdate(t: number, dt: number) {
    super.preUpdate(t, dt);
    // If the distance is less than 100, set a speed
    if (
      Math.Distance.BetweenPoints(
        { x: this.x, y: this.y },
        { x: this.target.x, y: this.target.y }
      ) < this.AGRESSOR_RADIUS
    ) {
      this.getBody().setVelocityX(this.target.x - this.x);
      this.getBody().setVelocityY(this.target.y - this.y);
    } else {
       // If the value is greater than 100, it will randomly go up or down
      const speed = 50;

      switch (this.direction) {
        case Direction.UP:
          this.getBody().setVelocity(0, -speed);
          break;

        case Direction.DOWN:
          this.getBody().setVelocity(0, speed);
          break;

        case Direction.LEFT:
          this.getBody().setVelocity(-speed, 0);
          break;

        case Direction.RIGHT:
          this.getBody().setVelocity(speed, 0);
          break; }}}Copy the code

In the preUpdate function set the monster to automatically move logic, distance less than 100, set a speed towards the hero, greater than 100, random 4 directions automatically move.

Render monsters according to anchor points

Next we need to instantiate the monster based on the anchor points created on the map. Add an initEnemies method to the Game scene to initialize monsters.

private initEnemies(): void {
// EnemyPoint on the past map
    const enemiesPoints = this.map.filterObjects(
      "Enemies".(obj) = > obj.name === "EnemyPoint"
    );
    // instantiate the monster
    this.enemies = enemiesPoints.map(
      (enemyPoint) = >
        new Enemy(
          this,
          enemyPoint.x as number,
          enemyPoint.y as number."lizard".this.player
        )
    );
    // Monsters and walls added collision checks
    this.physics.add.collider(this.enemies, this.wallsLayer);
    // Monsters and monsters added collision checks
    this.physics.add.collider(this.enemies, this.enemies);
    // Added collision checks for monsters and characters
    this.physics.add.collider(
      this.player,
      this.enemies,
      (obj1, obj2) = > {
          // After the collision callback, the character receives damage -1
        (obj1 as Player).getDamage(1);
      },
      undefined.this
    );
  }
Copy the code

This is where collision checking and post-collision callbacks are concerned. We can create characters and monsters on the map, and monsters can attack heroes, but our heroes can’t attack monsters.

Event notification

Therefore, we need to add event monitoring to the monster. When the distance between the monster and the character is less than the width of the character, it indicates a hit

this.attackHandler = () = > {
      if (
        Math.Distance.BetweenPoints(
          { x: this.x, y: this.y },
          { x: this.target.x, y: this.target.y }
        ) < this.target.width
      ) {
        this.getDamage();
        this.disableBody(true.false);// Stops the monster object body without disappearing

        this.scene.time.delayedCall(300.() = > {
          this.destroy();// Disappear after 300 milliseconds}); }};// EVENTS
    this.scene.game.events.on(EVENTS_NAME.attack, this.attackHandler, this);
    // Cancel listening after destruction
    this.on("destroy".() = > {
      this.scene.game.events.removeListener(
        EVENTS_NAME.attack,
        this.attackHandler
      );
    });
Copy the code

When the space bar is pressed, the Player tool animation is played and a global event is sent

if (this.cursors.space.isDown) {
      this.anims.play("attack".true);
      this.scene.game.events.emit(EVENTS_NAME.attack);
    }
Copy the code

Show food according to anchor points

Similar to rendering monsters, we can render some food on the map.

These materials were downloaded from Iconfont, and then assembled into sprites through Figma.

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

    this.chests = chestPoints.map((chestPoint) = >
      this.physics.add
        .sprite(
          chestPoint.x as number,
          chestPoint.y as number."food".Math.floor(Math.random() * 8)
        )
        .setScale(0.5));this.chests.forEach((chest) = > {
      this.physics.add.overlap(this.player, chest, (obj1, obj2) = > {
        this.game.events.emit(EVENTS_NAME.chestLoot);
        obj2.destroy();
      });
    });
  }
Copy the code

Like the monster, the food is rendered first according to the anchor point, but the callback is different when the hero and food collide, when the hero and food overlap, the player gets 10 points

Text display

Now let’s display an HP value above the character’s head.

import { Text } from './text'; . private hpValue: Text; .this.hpValue = new Text(this.scene, this.x, this.y - this.height, this.hp.toString())
  .setFontSize(12)
  .setOrigin(0.8.0.5); .update(){...this.hpValue.setPosition(this.x, this.y - this.height * 0.4);
  this.hpValue.setOrigin(0.8.0.5);
}
...
public getDamage(value?: number): void {
  super.getDamage(value);
  this.hpValue.setText(this.hp.toString());
}
Copy the code

The HP value will now be displayed above the game character, and in the update method we have updated the position of the HP text value so that the PLayer will not have a problem even if it moves.

The UI display

Finally, let’s add a UI scenario for displaying system prompts.

import { Scene } from "phaser";

import { EVENTS_NAME, GameStatus } from ".. /.. /consts";
import { Score, ScoreOperations } from ".. /.. /classes/score";
import { Text } from ".. /.. /classes/text";
import { gameConfig } from ".. /.. /";

export class UIScene extends Scene {
  privatescore! : Score;privategameEndPhrase! : Text;private chestLootHandler: () = > void;
  private gameEndHandler: (status: GameStatus) = > void;

  constructor() {
    super("ui-scene");

    this.chestLootHandler = () = > {
      this.score.changeValue(ScoreOperations.INCREASE, 10);

      if (this.score.getValue() === gameConfig.winScore) {
        this.game.events.emit(EVENTS_NAME.gameEnd, "win"); }};this.gameEndHandler = (status) = > {
      this.cameras.main.setBackgroundColor("Rgba (0,0,0,0.6)");
      this.game.scene.pause("game-scene");

      this.gameEndPhrase = new Text(
        this.this.game.scale.width / 2.this.game.scale.height * 0.4,
        status === GameStatus.LOSE
          ? ` failed! \n\n Click the screen to start again
          : ` victory! \n\n Click the screen to start again
      )
        .setAlign("center")
        .setColor(status === GameStatus.LOSE ? "#ff0000" : "#ffffff");

      this.gameEndPhrase.setPosition(
        this.game.scale.width / 2 - this.gameEndPhrase.width / 2.this.game.scale.height * 0.4
      );

      this.input.on("pointerdown".() = > {
        this.game.events.off(EVENTS_NAME.chestLoot, this.chestLootHandler);
        this.game.events.off(EVENTS_NAME.gameEnd, this.gameEndHandler);
        this.scene.get("game-scene").scene.restart();
        this.scene.restart();
      });
    };
  }

  create(): void {
    this.score = new Score(this.20.20.0);

    this.initListeners();
  }

  private initListeners(): void {
    this.game.events.on(EVENTS_NAME.chestLoot, this.chestLootHandler, this);
    this.game.events.once(EVENTS_NAME.gameEnd, this.gameEndHandler, this); }}Copy the code

Loading in a Loading scenario with a game scenario.

...this.scene.start("game-scene");
this.scene.start("ui-scene"); ...Copy the code

When the hero’s HP is 0, the screen will say “fail”.

The deployment of

I used Vercel deployment. Just upload Github and Vercel will deploy automatically. Then the domain name CNAME goes to cname.vercel-dns.com.

Demo address: game.runjs.cool/

Code repository: github.com/maqi1520/ph…

I have also deployed the following applications

Editor.runjs. cool/ MDX typeset editor CV. Runjs. cool/ online resume generator low-code.runjs.cool/ easy version low code platform and open source, if you help remember to click a star, thank you!

summary

At this point, the Phaser 3 mini game is 90 percent complete. The remaining 10 percent needs to be polished and refined to make the game more enjoyable, and more levels need to be designed to make it more rewarding for users. Through this article, we implemented a Phaser.js development H5 game from scratch. Includes Sprite map, Sprite table, design map, animation, collision check, event notification, etc.

I believe that through the above learning, IN the future work, I will have a certain understanding of similar H5 games, and can quickly develop a small game.

The last

Thanks to @dashuai old ape to help design the mask wizard figure, Dashuai also created the “Ape creative camp”, the group has a lot of development leaders can help each other to answer questions and exchange technology, at the same time, Dashuai will share outsourcing, do sideline, interested partners can leave a message “into the group”.

That’s all the content of this article. I hope this article is helpful to you. You can also refer to my previous articles or share your thoughts and experiences in the comments section.

This article first nuggets platform, source ma blog