preface

Recently, I was working on a h5 game project, so I needed to look at the front-end framework related to the game, and finally chose PIXI (I didn’t bother to pick). The fastest way to learn a framework is development, so I spent some time writing a simple game to get a general understanding of PIXI. There should be no problem dealing with some simple projects.

The demo address

  • Please set the PC to mobile mode
  • Image resources are web images for learning only

The game screen

preparation

Pixi basis

The Chinese document on Pixi.js (Huashengweilai.com) is relatively simple and only covers the basics of the game, but that’s enough for me

Stage 1.

Use pixi to create a stage where all game elements are displayed

import * as Pixi from 'pixi.js';

const app = new Pixi.Application();
// Put the stage in HTML
document.body.appendChild(app.view)
Copy the code

2. Load resources

Static resources need to be loaded by loader before they can be used. If we directly introduce resources to create sprites, it will have no effect. If the static resources are too large, we can wait for the loader to finish loading and perform page rendering.

import img from './assets/image/1.png';

// the loader can be chained, and the load must be executed at the end, otherwise it will not be loaded
// When loading resources, it is recommended to give a name, which is convenient to reference later
app.scene.loader.add('img', img).load();
// Loader loading progress
app.scene.loader.onProgress.add(() = > {
    console.log('loading... ')})// The loader is loaded successfully
app.scene.loader.onComplete.add(() = > {
    console.log('loading cpmplete')})Copy the code

3. Create a Sprite

After static resources are loaded, sprites are created and displayed in the stage. We can easily control the properties of sprites

// Create a Sprite
const img = new Pixi.Sprite(app.scene.loader.resources['img'].texture);
// Specify Sprite properties

/ wide/high
img.width = 100;
img.height = 100;

// x,y position
img.x = 100;
img.y = 100;

// Set the origin of the Sprite
img.anchor.x = 0.5;
img.anchor.y = 0.5;

// Set the zoom of the Sprite
img.scale.set(1);

// Set the Sprite rotation Angle
img.rotation = 1.2;

// Set the transparency of the Sprite
img.alpha = 0.9

// Put the Sprite on the stage
app.scene.stage.addChild(img);
Copy the code

4. ticker

Using the Ticker loop system, it’s easy to do some simple animations that are essentially similar to requestAnimationFrame

const ticker = Pixi.Ticker.shared;

const run = () = > {
    img.x += 1;
    img.y += 1;
    if (img.x > 100) {
    // Move the loop function out of ticker
        ticker.remove(run);
    };
};
// Add the loop function to the ticker
ticker.add(run);
// Stop all looping functions in ticker
ticker.stop();
// Start the loop
ticker.start();
Copy the code

5. Text elements

Construct a Text element with pixi.text

const options = {
    fontFamily: "Arial".fontSize: 48.fill: "white"};const msg = new Pixi.Text('wenben', options);

// Modify the text
msg.text = 'wenben1';
// Modify the style
msg.style = {};
Copy the code

Game design

For the first time to make a game, it is correct to start from simple. I choose to make a shooting game, with the content as simple as possible. The game ideas and gameplay are as follows:

  • Fix the cannon position, tap the area on the screen, rotate the muzzle to the corresponding Angle, and fire a shell
  • Two monster types, moving from top to bottom, removed after off-screen
  • Monster 1 collides with shell once, monster 1 and shell disappear, player scores one point
  • Monster 2 collides once with the cannonball, the monster becomes smaller, continues to move, the cannonball disappears, a second collision occurs, the monster disappears, and the player scores two points
  • Top left scoreboard

Matters needing attention:

  1. Random spawning of monster types and corresponding hit counts (health)
  2. Collision detection of monsters and shells
  3. Scoreboard updated
  4. Barrel rotation

Now that you know where you’re going, get started!

The game development

Project structure and development approach

This project uses singleton pattern and object orientation (false), the project construction is JS + webpack, originally used TS to write, but found that there is no need to write, so changed to JS. The project structure is as follows:

--- pixi-game  
   --- src  
        // Static resources
       --- assets 
        / / constant
       --- constant  
        / / component
       --- components  
        / / method
       --- utils  
    / / webpack configuration
    --- config
    // html
    --- public  
    / / the entry
    --- index.js
Copy the code

Create a global APP instance

A game has only one app instance and can only initialize one, so I used singleton mode here. All components will then bind this instance to themselves by default, making it easy to call app methods

class Game {
    // The default width of the stage
    stageWitdh = window.innerWidth;
    stageHeight = window.innerHeight;
    // State of the game
    state = {
        reword: 0.play: false.over: false.paused: false};// When creating the scoreboard, assign it to game
    msg = null;
    // Create an array of shells for collision detection
    bullets = [];
    // Initialize the pixi stage
    scene = new Pixi.Application({
        width: this.stageWitdh,
        height: this.stageHeight,
        transparent: true.backgroundColor: 0x00000});// Dynamically change the stage size as the window changes
    rerender() {
        this.scene.view.width = this.window.innerWidth;
        this.scene.view.height = this.window.innerHeight; }}/ / the singleton
function createGame() {
    let game;
    return () = > {
        if(! game) { game =new Game();
        }
        return game;
    };
};

export default createGame();
Copy the code

The cannon part

The whole cannon is divided into two parts, a fixed base and a barrel that can be rotated. It should be noted that by default, the rotation of Sprite is based on the upper left corner, which obviously does not meet the rotation of my cannon. Therefore, it is necessary to modify the origin of the barrel to make it rotate based on the center of the base.

class Connon {... instance = Game() ...constructor() {
      this.cannonBase = new Sprite('game_cannon_base');
      this.cannonBody = new Sprite('game_cannon_body'); .// Set the origin of the gun
      this.cannonBody.anchor.x = 0.5;
      this.cannonBody.anchor.y = 0.8;
      // Gun body origin coordinates, convenient later calculation Angle
      this.anchorPos = {
         x: window.innerWidth / 2.y: window.innerHeight - this.cannonBody.height * 2.
      }
      // Set a safe height to prevent the gun from rotating at will
      this.safeHeight = window.innerHeight - this.cannonBody.height*3/2
      // Enable listening
      this.listener();
   }

   listener() {
      window.addEventListener('touchstart'.this.touchEvent.bind(this))}/ / touch events
   touchEvent(e) {
       // Get the touch coordinates
      const { clientX, clientY } = e.changedTouches[0];
      if (clientY > this.safeHeight) {
         return;
      }
      // Calculate the rotation Angle
      const x = this.anchorPos.x - clientX;
      const y = this.anchorPos.y - clientY;
      const rotate = this.cannonBody.rotation ? (+this.cannonBody.rotation).toFixed(2) : 0;
      let nextRotate = -(x/y).toFixed(2); .// Rotation function
      const run = () = >{...if (rotationStep ==  nextRotate.toFixed(2) | |Math.abs(rotationStep) >= 1.2) {
            // When the rotation reaches the specified Angle or maximum Angle, it stops spinning and generates a shell
            ticker.remove(run);
            ticker.addOnce(() = > {
               new Bullet({
                  rotation: nextRotate,
                  y: this.anchorPos.y,
               })}
            );
            return; }...this.cannonBody.rotation = rotationStep; } ticker.remove(run); ticker.add(run); }}export default Connon;
Copy the code

The shell parts

When the rotation stops, a shell needs to be generated and fired at the specified Angle. The Angle of rotation of the shell is the same as the gun body. When the shell misses the target and exceeds the screen, it will be automatically removed.

class Bullet {
   // Shell generation is relatively simple, deleted
   // Focus on self detection
   run(r) {
      let stepX = r;
      let stepY = -1;
      const move = () = > {
         stepX += r;
         stepY += -1;
         this.bullet.x += stepX;
         this.bullet.y += stepY;

         if (this.bullet.x < -this.bullet.width || this.bullet.y < -this.bullet.height) { ticker.remove(move); }}const remove = () = > {
         ticker.remove(move);
         this.instance.scene.stage.removeChild(this.bullet);
      }
      // Bind a removal method to the shell itself, which will be used later
      this.bullet.remove = remove; ticker.add(move); }}export default Bullet;
Copy the code

The monster part

In this project, I found two pictures of monsters and used random numbers to create a type of monster randomly. The monster fell from different heights, causing a sense of delay. On the way of monster movement, I conducted collision detection and compared coordinates of all bullets on the current screen, and then made corresponding operations:

class Monster{
    instance = Game()
    speed = 6
    // Monster info
    monsterNames = [
        {name: 'game_monster_b'.hit: 2.scale: 1.8.reword: 2}, 
        {name: 'game_monster_s'.hit: 1.scale: 1.reword: 1}
    ]
    hitCount = 0;
    alphaStep = 0.1;
    constructor(props) {
        this.init();
    }
    init() {
        // Select a random index
        const randomIndex =Math.floor( Math.random() * 1 + 0.4);
        const info = this.monsterNames[randomIndex];
        this.ms = new Sprite(info.name);
        this.ms.info = info; .// Set random y coordinates
        this.ms.y = Math.random() * 200 - 400;
        this.instance.scene.stage.addChild(this.ms);
        this.run();
    }

    run() {
        // Move the method
        const move = () = > {
            this.ms['y'] + =this.speed;
            // Check for bullets
            if (this.instance.bullets.length) {
                for(let i = 0; i < this.instance.bullets.length; i++) {
                    // Perform collision detection
                    const isHit = bulletHit(this.ms, this.instance.bullets[i]);
                    if (isHit) {
                        // There is a collision, remove shells
                        this.hitCount += 1;
                        this.instance.bullets[i].remove();
                        this.instance.bullets.splice(i, 1);
                        i --;
                        // Determine if the maximum number of monster collisions is exceeded
                        if (this.hitCount >= this.ms.info.hit) {
                            // Remove monster, execute destroy function
                            ticker.remove(move);
                            this.destory();
                            return; }}}}// Make a simple animation of the hit count twice
            if (this.hitCount) {
                this.ms.alpha = 0.8;
                this.ms.scale.set(SCALE);
            }
            // Off screen, remove
            if (this.ms.y >= window.innerHeight) {
                ticker.remove(move);
                this.instance.scene.stage.removeChild(this.ms);
            }
        }
        ticker.add(move);
    }
    destory() {
        this.instance.state.reword += this.ms.info.reword;
        this.instance.msg.update();
        this.instance.scene.stage.removeChild(this.ms); }}export default Monster;
Copy the code

Collision function

Collision detection is an integral part of the game, many game engines have collision detection, but pixi does not have it, we need to develop our own. This game development only involves the collision of two objects, monster and shell. The origin of the shell is set as the center of the graph. We only need to judge whether the center point of the bullet is in the monster’s body during the monster’s movement.

export const bulletHit = (m, b) = > {
    const m_left = m.x;
    const m_right = m.x + m.width;
    const m_top = m.y;
    const m_bottom = m.y + m.height;
    // As long as the bullet's center point coordinates are included in the monster's body coordinates, the two are considered to have collided
    return m_left < b.x && m_right > b.x && m_top < b.y && m_bottom > b.y;
}
Copy the code

The scoreboard

The scoreboard is relatively simple, create a text, every time a monster is hit dead, the score increases, call the update method of text, display the latest score. The initialized scoreboard is mounted directly to the global app for ease of call.

class Text {
    instance = Game();
    constructor(options = {}) {
        this.text = new Pixi.Text(` total score:The ${this.instance.state.reword}`, {
            fontFamily: "Arial".fontSize: 48.fill: "white"});this.instance.msg = this;
        this.instance.scene.stage.addChild(this.text);
    }
    update() {
        this.text.text = ` total score:The ${this.instance.state.reword}`; }}Copy the code

complete

The game has been basically developed. Although the operation is stiff and there are still bugs in gun rotation, AS the first project of personal learning and development, I have taken the first step successfully. I learned a new technology stack from my work, improved my ability and expanded my knowledge reserve.

If you are interested, you can download the source code directly to GitLab (the code may be confused, there are some things I tested). Thank you!