In the recent project development, I needed to make a game similar to lipstick machine in the mall. I saw the recommended Phaser on GitHub as the game/animation framework, studied and developed a game, and recorded the learning process and problems encountered in the development process.

Let’s take a look at the final implementation:


## Start installing NPM Install Phaser

import Phaser, { Game } from 'phaser' import LoadScene from './scenes/LoadSence' constructor (props) { super(props) this.state = { AUTO, width: 1920, height: 1080, transparent: true, parent: 'canvas', scene: [// Set LoadScene...]  } window.game = new Game(this.state.config) }Copy the code

In Phaser3, the game is constructed with an object that contains the various configuration properties of the game.

The type property, which can be phaser. CANVAS, phaser. WEBGL, or Phaser.AUTO, determines the rendering mode of the game. The general recommended setting is phaser. AUTO, and the renderer will try to use WebGL mode, or Canvas mode if the browser or device does not support it.

Width and height are how Phaser sets the canvas’s size. You can specify the ID of the parent container, canvas div, in config.

In Phaser3, we add the scene to an array, which we then add to the Scene property of the Config object. By default, the game will automatically start from the first scene of the array.

At this point, a game instance is ready to load.

Based on the needs of the game, we are ready to define our first game scene.

import Phaser, {Scene} from 'phaser'
const launch = require('assets/img/needle/launch1.png')
const target = require('assets/img/needle/plate.png')
const sitRadish = require('assets/img/needle/radishActive.png')
const radish = require('assets/img/needle/radish.png')
const radishBg = require('assets/img/needle/rightBg.png')
const pass1 = require('assets/img/needle/pass1.png')
const pass2 = require('assets/img/needle/pass2.png')
const pass2A = require('assets/img/needle/pass2A.png')
const pass3 = require('assets/img/needle/pass3.png')
const pass3A = require('assets/img/needle/pass3A.png')

const json = {
  'knife': launch,
  'target': target,
  'sitRadish': sitRadish,
  'radish': radish,
  'radishBg': radishBg,
  'pass1': pass1,
  'pass2': pass2,
  'pass2A': pass2A,
  'pass3': pass3,
  'pass3A': pass3A
}

class LoadSence extends Scene{
      constructor(){
        super("load")
      }
      preload(){
          this.loadAssets(json)
      }
     loadAssets (json) {
        Object.keys(json).forEach((key) => {
          this.load.image(key, `${json[key]}`)
        })
      }
      create(){
         this.scene.start('passN', {number: 0})
      }
      update(){

      }
    }
Copy the code

In a Phaser game, the scene is created by running the preload function, which preloads the image, then by running the create function, which performs the initialization code, and finally by calling the Update function at each step.

In the game I define the first scene to load the images I need. I encountered the first problem while loading the image. URL console.log is a base64 format (Local data URIs are not supported… This is related to the configuration of urL-loader in the image resources that Webpack handles.

 {
  test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
  loader: require.resolve('url-loader'),
  options: {
  limit: 10000,
  name: 'static/media/[name].[hash:8].[ext]'
  }
 }
Copy the code

Limit specifies the size of the image. If the value is smaller than the set size, the image will be automatically converted to Base64 format, causing a loading failure. According to the requirements of the game, the game has three levels, so at the beginning of the game to determine which level, there is a second scene passN.

import { Scene } from 'phaser'

class PassScene extends Scene {
  constructor () {
    super('passN')
  }
  init (data) {
    this.number = data.number
  }
  create () {
  } 
}
Copy the code

This involves transferring values between scenes, adding sprit diagrams, adding text, and configuring tween. ######1. Transfer values between scenarios

This.scene. start('passN', {number: 0});Copy the code

######2. Create a Sprite and add text

// Customize the font @font-face {font-family: 'Needle' SRC: url('~assets/needle.ttf') font-weight: normal font style: Create () {var passN = [' one ', 'two ', Sprite = this.add. Sprite (window.game.config.width / 2, 0, 'passPng') var config1 = {x: Width / 2-110, y: 0, text: '#' + passN[this.number] + '# ', style: {fontSize: '76px', fontFamily: 'Needle', fill: '#f08f3f', align: 'center' } } this.text = this.make.text(config1) }Copy the code

######3. Create a tween animation

GameStart () {this.tweens.add({targets: [this.sprite, this.text],//tweens ease: 'Bounce',// movement y: Window. Game. Config. Height / 2, / / y coordinates of the target, duration: 1000, / / animation time callbackScope: this, / / this value the onComplete callback function: () = > {/ / completion callback function setTimeout (() = > {this. Scene. The start (' game ', {pass: this number})}, 500)}})}Copy the code

Tween animation in Phaser3 is particularly convenient, as we can animate an object by simply passing the configuration properties of the tween animation. The code above is a simple animation that moves text and Sprite graphics from the top of the screen to the middle of the screen, and when it’s done, it’s ready for the game.

Below is the main scene of the game, there are rotating plates, carrots launched from the right, and the creation of a group of turnip sprites, each level of 15s countdown. Options is the configuration of level 3 games.

Const options = [{// first level rotationSpeed: 3,// rotationSpeed: 150,// minAngle: 20,// rotation distance radishNum: RotationSpeed: 4.5, throwSpeed: 150, minAngle: 20, radishNum: 8}, {// rotationSpeed: 4.5, throwSpeed: 150, minAngle: 20, radishNum: 8}, {// rotationSpeed: 4, throwSpeed: 150, minAngle: 20, radishNum: 8 } ]Copy the code

Then create the game scene in the game.

import Phaser, { Scene } from 'phaser' import options from '.. /constants/options' class GameScene extends Scene { constructor () { super('game') } preload () { } init (data) { This.pass = data.pass this.translate = true} create () {// background this.radishbg = this.add.sprite(window.game.config.width - 241 - 163, -30, 'radishBg').setOrigin(0) this.total = 15 var config1 = { x: 1570, y: 130, text: '15秒', style: {fontSize: '70px', fontFamily: 'Needle', fill: '#f08f3f', align: 'Center'}} this.secondText = this.make. Text (config1) // Create timer this.time.adDevent ({delay: 1000, callback: callback) this.updateCounter, callbackScope: this, loop: KnifeGroup = this.add. Group () this.knife = true this.add.sprite(window.game.config.width / 2 + 400, window.game.config.height / 2, 'knife') this.target = this.add.sprite(window.game.config.width / 2 - 250, window.game.config.height / 2, 'target') this.target.depth = 1 // radish group this.radishGroup = this.add.group() This.creatradish (options[this.pass].radishnum) // event this.input.on('pointerdown', this.throwknife, Total -- var textNUM = this.total < 10? '0' + this.total + 'seconds' : This.total + 'seconds' this.secondText.settext (textNUM) else {// failed event}} // create creatRadish (n) {let x = 1537 + 50 let y = 270 let offsety = n === 6 ? 90 : 70 for (var i = 0; i < n; i++) { y = y + offsety this.add.sprite(x, y, 'sitRadish').setOrigin(0) this.radishGroup.add(this.add.sprite(x, y, 'radish). SetOrigin (0))}} destroySprite () {const children = this. RadishGroup. The getChildren () / / destruction of elves Children [0]. Destroy the update () ()} {const children = this. KnifeGroup. The getChildren () / / change the if (this. Pass = = = 2 && this.knifeGroup.children.size > 2 && this.knifeGroup.children.size < 5) || this.pass === 1) { this.target.angle -= options[this.pass].rotationSpeed this.knifeSpeed(children, 0)} else {// target speed this.target. Angle += options[this.pass]. KnifeSpeed (children, addOrSub) {for (let I = 0; i < children.length; If (addOrSub) {children[I]. Angle += options[this.pass]. RotationSpeed} else {children[I]. Angle -= Options [this.pass].rotationSpeed} // Knife Position of Sprite group const radians = phaser.math.degtorad (children[I].angle) children[I].x = this.target.x + (this.target.height / 2) * Math.cos(radians) children[i].y = this.target.y + (this.target.height / 2) * Math.sin(radians) } } throwKnife () { if (! This.canthrow) {return} this.canthrow = false // knife Throw animation this.tweens.add({targets: [this.knife], // ease: 'Bounce', x: this.target.x + this.target.height / 2, duration: options[this.pass].throwSpeed, callbackScope: this, onComplete: this.onCompleteThrowKnife }) } onCompleteThrowKnife () { let legalHit = true const children = This. KnifeGroup. GetChildren () / / destroyed placeholder radish enclosing destroySprite Angle () / / use to judge whether insert for success (let I = 0; i < children.length; i++) { const isSameAngle = Math.abs(Phaser.Math.Angle.ShortestBetween(this.target.angle, children[i].impactAngle)) < options[this.pass].minAngle if (isSameAngle) { legalHit = false break } } if (legalHit) { this.canThrow = true const knife = this.add.sprite(this.knife.y, this.knife.x, 'knife') knifegroup.impactangle = this.target.angle this.knifegroup.add (knife) // Knife returns to original position this.knife.x = window.game.config.width / 2 + 400 if (this.knifeGroup.children.size >= options[this.pass].radishNum) { this.knife.destroy() this.canThrow = false if (this.pass < 2) { setTimeout(() => { this.changePass(this.pass + 1) this.scene.start('passN', {number: This.pass})}, 500)} else {// successful events}}} else {// Knife collision to fly out animation this.tweens.add({targets: [this.knife], x: this.sys.game.config.width + this.knife.height, rotation: 5, duration: Options [this.pass]. ThrowSpeed * 4, callbackScope: this, onComplete: () => {// failed event}})}}}Copy the code

######1. Create the target dish and Goup object set to rotate the target dish and execute the update function.

this.target.angle += options[this.pass].rotationSpeed
Copy the code

Create a collection of launched radishes and the reserved radishes on the right. Destroy one radish on the right before each launch. ######2. Radish emission logic uses a listener function to control radish emission.

this.input.on('pointerdown', this.throwKnife, this)
Copy the code

Check to see if it can be launched when the mouse is pressed down, and if so, add a Tweens animation to the radish. When it is launched on the plate, check to see if it overlaps with other radishes.

At each launch, the Angle of the current disk is saved, and whether the distance between the rotation degree of the current disk and the previous rotation degree is less than the minimum value at the next launch. If it is less than the minimum value, the failure function will be executed, otherwise it will continue.

When the turnip throw is complete, proceed to the next level (passN scenario) or the challenge succeeds.

Start ('passN', {number: this.pass}) // Destroy the game this.sys.game.destroy(true)Copy the code

######3. Update the position of the function in the game set the first level of the plate clockwise, the second level counterclockwise, the third level clockwise rotation.

In update, the position of the child radish in each knifeGroup is traversed, and the offset Angle of each step is calculated when the radish moves, so as to determine the X and Y displacement of child radish child.

Const radians = phaser.math.degtorad (children[I].angle) children[I].x = this.target.x + (this.target.height / 2) * Math.cos(radians) children[i].y = this.target.y + (this.target.height / 2) * Math.sin(radians)Copy the code

######4. Set the game time for each level to 15s, so create a timer and the game will fail when the time ends.

This.time. addEvent({delay: 1000, callback: this.updatecounter, callbackScope: this, loop: true})Copy the code

######5. others Because in Phaser3, the origin of all objects defaults to their center, you can change the default origin with setOrigin. For example: this.add.sprite(x, y, ‘sitRadish’).setorigin (0) resets the origin to the upper left vertex of the image, which will simplify the calculation during drawing.

The initial version of Phaser was ^3.9, and an error was reported when destroying this.sys.game.destroy(true). The problem was identified as a bug in github Issues, which was later updated to the latest version. At this point, the development of this little game is over.