Open dry

Technology stack: Canvas, Phaser, TS

Canvas’s small interactive games are an important manifestation of interactive marketing. We gradually understand, master, familiar with H5 game development.

Push box small game is a lot of people’s childhood memories, we will use a simple code to achieve it.

Push box requirement

Thinking: 1, need a code development environment, 2, need a Canvas engine, 3, need to prepare a character to be our hero, a box, a box push end, a burning box, and our game map (wall).

1. Infrastructure Construction (TS+ Phaser)

The game engine is just for us to better use Canvas. Those who are interested can learn about pixi, Egret, Phaser, Laya, Cocos and other excellent game engines. Its application is not limited to games. Since it is not open source yet, we use phaser to implement this little game.

The basic framework is clean and there is nothing. Down a clean template. The Phaser + TS project template is at the end of the source code.

Load static resources

First, what does it take to complete a game of pushing boxes?

1. Prepare materials

The wall, the box, the burning box, the end of the box, the characters

Edit the scene map

For extensibility, we use a 20 by 20 two-dimensional array to edit our map. Define the data mapping relationship. 0 — Background 1 — Wall 2 — floor 3 — box 4 — End 5 — Person 6 — box on fire

  • src/constants/types.ts
/** Grid 0 without 1 wall 2 floor 3 box 4 box finish 5 people 6 burning box */
export enum BLOCK {
    wall = 1,
    ground = 2,
    box = 3,
    end = 4,
    player = 5,
    badBox = 6
}
Copy the code

Let’s edit the first level map first.

  • src/constants/config.ts
export const LEVEL = {
  LV_1: [[0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.1.1.1.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.1.4.1.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.1.2.1.1.1.1.0.0.0.0.0.0],
  [0.0.0.0.0.0.1.1.1.3.2.3.4.1.0.0.0.0.0.0],
  [0.0.0.0.0.0.1.4.2.3.5.1.1.1.0.0.0.0.0.0],
  [0.0.0.0.0.0.1.1.1.1.3.1.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.1.4.1.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.1.1.1.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0],
  [0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0]]}Copy the code

3. Create the material configuration file

  • src/constants/config.tsAnd will be prepared in advance of several material definition
/** Resource allocation */
export const RES = {
  wall: "//yun.duiba.com.cn/spark/assets/5fe60952f141c695902a1a7428bc4bb1c254627c.jpeg".box: "//yun.duiba.com.cn/spark/assets/c68150a7c6c0aaa9c88d64ac4ea51f26d9dfb386.jpeg".end: "//yun.duiba.com.cn/spark/assets/9aa5d0732bea222d58347bf435e7f67a851b9fc1.png".player: "//yun.duiba.com.cn/spark/assets/74a309f60978ba65bba21915ec75b2e9310dcc75.png".badBox: "//yun.duiba.com.cn/spark/assets/de6a7e6f261e74568acffe56ebe5bf8ffe5f49b2.png".resetBtn: "//yun.duiba.com.cn/spark/assets/c684fbca6528b566d2e65b9c491b9a23773b9aed.png"
}
Copy the code

4. Preload static images

  • src/scenes/preloadScene.ts
import { RES } from '.. /constants/config'

export default class PreloadScene extends Phaser.Scene {
  constructor() {
    super({ key: 'PreloadScene'})}preload() {
    // RES.map(item =>)
    for (const key in RES) {
      if (Object.prototype.hasOwnProperty.call(RES, key)) {
        this.load.image(key, RES[key])
      }
    }
  }

  create() {
    this.scene.start('MainScene')}}Copy the code

Third, build a simple stage

New scenes/SRC/mainScene. Ts

MainScene;

import { LEVEL } from '.. /constants/config'
import { DIR, BLOCK, KEY_DIR } from '.. /constants/types'


export default class MainScene extends Phaser.Scene {
  /** Map container */
  private mapContainer: Phaser.GameObjects.Container
  /** What is the current step of copy */
  private stepText: Phaser.GameObjects.Text
  /** Level copy */
  private lvText: Phaser.GameObjects.Text
  private curMap: number[] []/** The position of the current character (in a two-dimensional array) */
  private playerPointer: { [key: string] :number } = { x: 0.y: 0 }

  private _lv: number = 1
  private _step: number = 0
  private get step() :number {
    return this._step
  }
  private set step(v: number) {
    this._step = v
    this.stepText && (this.stepText.text = 'steps' + v)
  }

  private get lv() :number {
    return this._lv
  }
  private set lv(v: number) {
    this._lv = v
    this.lvText && (this.lvText.text = 'levels' + v)
  }
  constructor() {
    super({ key: 'MainScene'})}create() {
    this.initGame()
  }

  /* initialize game */
  initGame() {
    this.step = 0
  }

  /** Restart the level */
  resetGame() {
    this.initGame()
  }

  /** Determine whether to terminate */
  private gameOver() {
    const mapList = LEVEL[`LV_The ${this.lv}`]
    for (let i = 0; i < mapList.length; i++) {
      for (let j = 0; j < mapList[i].length; j++) {
        if (mapList[i][j] == BLOCK.end && this.curMap[i][j] ! = BLOCK.box) {return false}}}return true
  }
  update(){}}Copy the code

4. Map

Here we draw the scene, using a data mapping relationship, each cell is 80*80; The idea is to update the data structure. With each step, we are updating our map, rendering the new map with new 2d arrays and visually pushing the box.

New utility class: gutils. copyArray is just an array copy method. I can define it anywhere I want

export class GUtils {
  /** Copy array */
  static copyArray(arr: Array<any>) {
      let newArr:any = [];
      for (var i = 0; i < arr.length; i++) {
          newArr[i] = arr[i].concat();
      }
      returnnewArr; }}Copy the code

1. Screen out the current level map

    /* initialize game */
  initGame() {
    this.step = 0
    this.curMap = GUtils.copyArray(LEVEL[`LV_The ${this.lv}`])
    this.renderMap()
  }
Copy the code

2. As shown in the picture above, we are ininitGame()Method to implement the render map methodthis.renderMap()

There’s a new method called renderMap that we’re going to render based on the values of the two-dimensional array. Grid 0 no 1 wall 2 floor 3 box 4 box finish 5 people 6 burning box. Use vector Graphics for Spaces and Sprite for other cells


  /** Create map */
  private renderMap() {
    this.mapContainer && this.mapContainer.removeAll()
    this.mapContainer = this.add.container(-425, -100.new Phaser.GameObjects.Container(this))
    this.curMap.forEach((colum, i) = > {
      colum.forEach((row, j) = > {
        switch (row) {
          case 0:
            let block = new Phaser.GameObjects.Graphics(this)
            this.mapContainer.add(block)
            block.fillStyle(0x73ad45.1).fillRect(0.0.80.80)
            block.setPosition(j * 80, i * 80)
            break
          case 1:
            let wall = new Phaser.GameObjects.Sprite(this, j * 80, i * 80.'wall').setOrigin(0.0)
            this.mapContainer.add(wall)
            wall.displayWidth = wall.displayHeight = 80
            break
          case 2:
            break
          case 3:
            let box = new Phaser.GameObjects.Sprite(this, j * 80, i * 80.'box').setOrigin(0.0)
            this.mapContainer.add(box)
            box.displayWidth = box.displayHeight = 80
            break
          case 4:
            let end = new Phaser.GameObjects.Sprite(this, j * 80, i * 80.'end').setOrigin(0.0)
            this.mapContainer.add(end)
            end.displayWidth = end.displayHeight = 80
            break
          case 5:
            let player = new Phaser.GameObjects.Sprite(this, j * 80 + 10, i * 80.'player').setOrigin(0.0)
            this.mapContainer.add(player)
            player.displayWidth = 212 * 0.25
            player.displayHeight = 329 * 0.25
            this.playerPointer.x = i
            this.playerPointer.y = j
            break
          case 6:
            let badBox = new Phaser.GameObjects.Sprite(this, j * 80, i * 80.'badBox').setOrigin(0.0)
            this.mapContainer.add(badBox)
            badBox.displayWidth = badBox.displayHeight = 80
            break}})})}Copy the code

5. Start pushing the box

We need to use phaser keyboard events here. Judge up W, down S, left A, right D by the callback E.keey.

Add a listener for keyboard events to the create method.

this.cursors = this.input.keyboard.createCursorKeys()
this.input.keyboard.on('keydown'.this.onListenerKeyDown.bind(this))
Copy the code

Added the onListenerKeyDown event method

  /** Listen for keyboard events */
  private onListenerKeyDown(e) {
    if (!Object.values(KEY_DIR).includes(e.key)) return
    let p1 = {
      x: 0.y: 0
    }
    let p2 = {
      x: 0.y: 0
    }
    switch(e? .key) {case KEY_DIR.UP:
        this.dir = DIR.UP
        p1.x = this.playerPointer.x - 1
        p2.x = this.playerPointer.x - 2
        p1.y = p2.y = this.playerPointer.y
        break
      case KEY_DIR.LEFT:
        this.dir = DIR.LEFT
        p1.x = p2.x = this.playerPointer.x
        p1.y = this.playerPointer.y - 1
        p2.y = this.playerPointer.y - 2
        break
      case KEY_DIR.DOWN:
        this.dir = DIR.DOWN
        p1.x = this.playerPointer.x + 1
        p2.x = this.playerPointer.x + 2
        p1.y = p2.y = this.playerPointer.y
        break
      case KEY_DIR.RIGHT:
        this.dir = DIR.RIGHT
        p1.x = p2.x = this.playerPointer.x
        p1.y = this.playerPointer.y + 1
        p2.y = this.playerPointer.y + 2
        break
    }

    this.onCheckMove(p1, p2)
  }
Copy the code

Above we have listened to the keyboard event; And you see that there are two coordinates p1 and P2. However, we cannot move directly. To determine whether a cell can be updated (pushed), we need to determine the next position and the position after that. So we need to keep track of these two array locations:

1. What situation can’t we push?

  • Push in the direction of the wall
  • The next position in the same direction of the box is the box, the wall (here divided into the burning box and the target box)

2. Under what circumstances can we move?

  • The next position is open space
  • The next position is the box end
  • The next position is the box, and the next position is the open space
  • The next position is the box, and the next position is the box end (divided into two boxes)

3, pay attention to

  • When the current character position is the end of the box, you need to restore

And then we can write the code.


  /** Judge movement */
  onCheckMove(p1: { [key: string] :number }, p2: { [key: string] :number }) {
    const { x: x1, y: y1 } = p1
    const { x: x2, y: y2 } = p2
    const { x, y } = this.playerPointer
    // The wall cannot be moved
    if (this.curMap[x1][y1] == BLOCK.wall) {
      return false
    }
    //1 box 2 walls cannot be moved
    if (
      this.curMap[x1][y1] == BLOCK.box &&
      (this.curMap[x2][y2] == BLOCK.wall || this.curMap[x2][y2] == BLOCK.box || this.curMap[x2][y2] == BLOCK.badBox)
    ) {
      return false
    }
    //6 The fire box cannot be moved
    if (
      this.curMap[x1][y1] == BLOCK.badBox &&
      (this.curMap[x2][y2] == BLOCK.wall || this.curMap[x2][y2] == BLOCK.box || this.curMap[x2][y2] == BLOCK.badBox)
    ) {
      return false
    }

    / / floor
    if (this.curMap[x1][y1] == BLOCK.ground || this.curMap[x1][y1] == BLOCK.end) {
      this.curMap[x][y] = BLOCK.ground
      this.curMap[x1][y1] = BLOCK.player
    }
    // 1 Box floor
    if (this.curMap[x1][y1] == BLOCK.box && this.curMap[x2][y2] == BLOCK.ground) {
      this.curMap[x][y] = BLOCK.ground
      this.curMap[x1][y1] = BLOCK.player
      this.curMap[x2][y2] = BLOCK.box
    }
    //1 box and destination
    if (this.curMap[x1][y1] == BLOCK.box && this.curMap[x2][y2] == BLOCK.end) {
      this.curMap[x][y] = BLOCK.ground
      this.curMap[x1][y1] = BLOCK.player
      this.curMap[x2][y2] = BLOCK.box
    }

    // Fire box and floor
    if (this.curMap[x1][y1] == BLOCK.badBox && this.curMap[x2][y2] == BLOCK.ground) {
      this.curMap[x][y] = BLOCK.ground
      this.curMap[x1][y1] = BLOCK.player
      this.curMap[x2][y2] = BLOCK.badBox
    }

    // Fire box and end
    if (this.curMap[x1][y1] == BLOCK.badBox && this.curMap[x2][y2] == BLOCK.end) {
      this.curMap[x][y] = BLOCK.ground
      this.curMap[x1][y1] = BLOCK.player
      this.curMap[x2][y2] = BLOCK.end
    }

    if (LEVEL[`LV_The ${this.lv}`][x][y] == BLOCK.end) {
      this.curMap[x][y] = BLOCK.end
    }
    this.step++

    // Re-render
    this.renderMap()
    if (this.gameOver()) {
      this.lv++
      this.initGame()
    }
  }
Copy the code

The new 2d array is then used to render the scene. At this point we’re almost done with the push box.

6. Judgment results

What counts as a pass? Compare the level array to the current array. When the end of each box has become a box when the clearance. Of course, we can also add a step limit.

  /** Determine whether to terminate */
  private gameOver() {
    const mapList = LEVEL[`LV_The ${this.lv}`]
    for (let i = 0; i < mapList.length; i++) {
      for (let j = 0; j < mapList[i].length; j++) {
        if (mapList[i][j] == BLOCK.end && this.curMap[i][j] ! = BLOCK.box) {return false}}}return true
  }
Copy the code

You can also add a level and step display.

Switch levels

How do we get to the next level? It’s actually updating the data. Data relationship mapping is good, direct level redrawing can be. Also, you can add a button that you can click to replay the level.

 /* initialize game */
initGame() {
    this.step = 0;
    this.curMap = GUtils.copyArray(LEVEL[`LV_The ${this.lv}`]);
    this.renderMap();
}

/** Restart the level */
resetGame() {
    this.initGame();
}
Copy the code

At this point, the job is done. The following will continue to output H5 game combat, please pay attention to

Attached source address :github.com/superBlithe…