Codesandbox. IO/s/tap – bird -… Visit the page to see the effect

Each step is coded separately and can be read against this code to make it easier to understand

In the early identification of PIXI

There are many articles on the web, so I won’t expand them here. If you don’t know about game engines, please read the following

H5 Scene small animation implementation PixiJs actual combat – Zhihu (zhihu.com)

API Manual PixiJS API Documentation

start

Material preparation

Our images are stored in public/assets, and the compiled relative program file is read in a path like./assets/bird.png

Step 1 Set the stage

All of the game’s visual actions are inside the canvas, and for the sake of coordinate conversion, let’s assume that our screen width is 750 and height 1334, meaning that a 750 x 1334 image can be spread across the entire game. So our canvas is going to look like

<canvas width="750" height="1334" />
Copy the code

In the real world, however, not all devices are 750 x 1334, so we might have to scale, such as x0.5 if the screen width is 375. We use the width and height properties of style for control

<canvas width="750" height="1334" style="width: 375px; height: 667px" />
<! -- width, height denotes the pixel size inside the canvas, corresponding to the corresponding pixel in the memory area -->
<! > < span style = "font-size: 14px! Important; color: RGB (51, 51, 51);
Copy the code

With the initialization method of pixi, so

// this.$refs.canvas is the corresponding canvas node
application = new PIXI.application({
  width: 750.height: 1334.view: this.$refs.canvas,
  backgroundColor: 0x0099ff.// Make it blue for easy observation
});

this.$refs.canvas.style.width = this.width + "px";
this.$refs.canvas.style.height = this.height + "px";
Copy the code

Step 2 Experiment by adding a background

Once the stage is created, we can try to add some view elements to the stage. PIXI was developed in an object-oriented manner, using different classes for different visual elements, such as

The name of the class describe
Sprite The elves class. General image rendering and so on
Text The text class. Mainly used for rendering text
AnimatedSprite Compared to sprites, more animation frame control, generally used to do frame by frame animation elements, such as walking people, flying birds
Container Container, mainly used for container wrapping

For example, Container and Sprite inherit from DisplayObject, while AnimatedSprite inherit from Sprite. Therefore, many property methods and concepts are the same

In the game engine world, we use images (collections of pixels) as materials, and when we initialize a Sprite class, we need to load our background image, generate a material object, and use it for initialization

// Initialize the material and pull it from the static file
const bgTexture = Texture.from("./assets/bg.png"); 

// Create a Sprite object
const bg = new PIXI.Sprite(bgTexture);

// Configure dimensions, coordinates, etc
bg.width = 750;
bg.height = 1334;
bg.x = 0;
bg.y = 0;

// The stage we operate on is the stage in the app
application.stage.addChild(bg);
Copy the code

The generation of materials, in addition to images, can also

  1. By pulling from an existing material, you can cut an existing material
  2. You can use the drawing tool class directly to draw simple graphics, such as a circle or a triangle
  3. It can also be taken directly from the display object. For example, we can take an image of the entire game interface, generate a new material, and then store it as a picture and make it into a screenshot

For this code, you can look at tap-bird-CodesandBox

Step 3 Add flooring

Next we add a floor. We created a container and used two images of the floor end to end to make it a longer image for easy animation control

const ground = new Container();
const texture = Texture.from("./assets/ground.png");

// Add a floor
// Create two floors to form a single base plate for loop scrolling
const ground1 = new Sprite(texture);
ground1.x = 0;

const ground2 = new Sprite(texture);
ground2.x = ground1.width;

ground.height = ground1.height;
ground.width = 2 * ground1.width;
ground.addChild(ground1);
ground.addChild(ground2);
ground.y = 1334 - ground.height;
app.stage.addChild(ground);
Copy the code

Note that the floor may not appear, because the resource load is not timely, causing some exceptions in width and height access, resulting in the size and position of the element, you can switch the refresh effect several times, we will solve these problems in the fourth step

For this code, you can look at tap-bird-CodesandBox

Step 4 optimize code structure and lazy loading

So far, we’ve found some problems

  1. The resource loading delay may cause the interface to display abnormally
  2. In the future, we need to add a lot of class creation and logic code, which is not easy to maintain in one process

In view of the above problems, we make the following adjustments

The customApplicationClass instead ofPIXI.Application

This part of the work is to move the initialization logic into and out of the entry code

// In the vue file, introduce a custom class
import Application from './Application';

// Mounted
{
  mounted() {
    this.app = new Application({
      width: 750.//this.width,
      height: 1334.//this.height,
      view: this.$refs.canvas,
      backgroundColor: 0xffffff,})this.$refs.canvas.style.width = this.width + "px";
    this.$refs.canvas.style.height = this.height + "px"; }},Copy the code

Write the Application class

import {
  Application
} from "pixi.js";

// Inherited from pixi.Application, unpopulated with initialization logic
export default class game extends Application {
  constructor(props) {
    super(props);

    // todo initializes logic}}Copy the code

useLoaderResources are preloaded

Before the initialization stage, we use the Loader of PIXI to preload resources. After the loading is completed, we will create elements and other work, so the Application class needs to be changed

import {
  Application,
  Loader,
  Sprite
} from "pixi.js";

// Inherited from pixi.Application, unpopulated with initialization logic
export default class game extends Application {
  constructor(props) {
    super(props);
    
    // Resource loading allows cross-domain, avoiding image error
    Loader.shared.add({ url: "./assets/bg.png".crossOrigin: true });
    Loader.shared.add({ url: "./assets/ground.png".crossOrigin: true });
    Loader.shared.add({ url: "./assets/bird.png".crossOrigin: true });
    Loader.shared.add({ url: "./assets/holdback.png".crossOrigin: true });
    Loader.shared.add({ url: "./assets/number_2.png".crossOrigin: true });
    
    // Trigger init after loading. Use bind to prevent this pointer from changing
    Loader.shared.onComplete.once(this.init.bind(this));
    
    // Start loading
    Loader.shared.load();
  }
  
  init(){
    // Initialize the logic
    // Add background
    // The way the material is referenced has changed
    const bg = new Sprite(Loader.shared.resources["./assets/bg.png"].texture);
    bg.width = 750;
    bg.height = 1334;
    bg.zIndex = 0;
    this.stage.addChild(bg); }}Copy the code

After this adjustment, the way materials are referenced has changed

/ / the original
const bg = new Sprite(Texture.from("./assets/bg.png"));

/ / new
const bg = new Sprite(Loader.shared.resources["./assets/bg.png"].texture);
Copy the code

Move the floor

Following the previous thought, we’ll wrap Ground as a separate class and add a new method to it, onUpdate, to update the floor roll position

import { Container, Sprite, Loader } from "pixi.js";

export default class Ground extends Container {
  constructor(props) {
    super(props);
    // See the previous code
  }

  onUpdate() {
    // Each scroll reduces the entire box to the left (negative) by 10 px, reset to 0 for more than one screen
    this.x = (this.x - 10) % 750; }}Copy the code

We then call it in the Application with the tick to refresh

export default class game extends Application {
  init() {
  
    // Add new floor and position it
    this.ground = new Ground();
    this.ground.y = 1334 - this.ground.height;
    this.ground.x = 0;
    this.stage.addChild(this.ground);

    // tick
    this.onUpdate = this.onUpdate.bind(this);
    this.ticker.add(this.onUpdate);
  }

  onUpdate() {
    // Refresh the floor
    this.ground.onUpdate(); }}Copy the code

Step 6 Add obstacles

An obstacle with an upper and lower tube

And our material file is

Remember the source of the material, we can split an image to produce two materials left and right. Combined with the previous use of Container, we pack the top and bottom tubes in one Container

import { Container, Sprite, Texture, Loader } from "pixi.js";

export default class Holdback extends Container {
  constructor(offset) {
    super(a);const HOLDBACK_TEXTURE =
      Loader.shared.resources["./assets/holdback.png"].texture;
      
    // Material clipping generates two pipe objects
    this.top = new Sprite(
      new Texture(HOLDBACK_TEXTURE, {
        x: HOLDBACK_TEXTURE.width / 2.y: 0.width: HOLDBACK_TEXTURE.width / 2.height: HOLDBACK_TEXTURE.height
      })
    );

    this.bottom = new Sprite(
      new Texture(HOLDBACK_TEXTURE, {
        x: 0.y: 0.width: HOLDBACK_TEXTURE.width / 2.height: HOLDBACK_TEXTURE.height
      })
    );
    
    // Use the input parameter to locate the two pipes
    // Anchor. Set is the center of the set object, with 1 meaning 100%
    this.top.anchor.set(0.1);
    this.top.y = offset;
    this.bottom.y = this.top.y + 300;
    this.addChild(this.top);
    this.addChild(this.bottom); }}Copy the code

Then we added dynamic create and destroy logic to the main logic code

onUpdate(){
    // This. Holdbacks is used to hold an array of obstacles that have been created
    let temp = [];
    for (let i = 0; i < this.holdbacks.length; i++) {
      const holdback = this.holdbacks[i];
      
      // Refresh the position
      holdback.x = holdback.x - 10;
        
      // Determine if it is completely out of the screen
      if (holdback.x <= -holdback.width) {
        // Delete from the stage
        this.stage.removeChild(holdback);
      } else {
        // Put it in the cachetemp.push(holdback); }}// Save the undeleted items again and continue to judge in the next cycle
    this.holdbacks = temp;
    
    // Create obstacles
    this.dist -= 10;
    if (this.dist % 300= = =0) {
      GetNextOffset generates a number that controls the location of the notch
      const holdback = new Holdback(this.getNextOffset());
      holdback.x = 750;
      holdback.zIndex = 2;
      
      // Insert into the middle of the stage
      this.stage.addChild(holdback);
      this.holdbacks.push(holdback);
      this.dist = 0; }}Copy the code

The getNextOffset method is generated using sin based on time

  getNextOffset() {
    return (
      Math.sin((((Date.now() - this.startTime) % 3000) / 3000) * 2 * Math.PI) *
        100 +
      350
    );
  }
Copy the code

Step 7 Draw layers

Now you can see that the floor is blocked because in Pixi, the render level is related to the insertion order, so we need to add a render level to them and sort them

We give the element zIndex when it is created and then sort it in the update phase

this.ground = new Ground();
this.zIndex = 1;

const holdback = new Holdback(this.getNextOffset());
holdback.zIndex = 0;

/ / sorting
this.stage.children.sort((a, b) = > a.zIndex - b.zIndex);
Copy the code

PIXI does not support this function, and the zIndex property is not used for this purpose

Add birds

  1. useAnimatedSpriteClass to create birds
  2. At any time the bird has a velocity vy, which starts at 0, which should be 0 every time it updates its positionthis.y += vy
  3. The speed of the bird is not constant, it is affected by gravity, so we have to update the speed every time we refresh the positionthis.vy += a, a is constant and can be adjusted as needed
  4. When you tap the screen again, the bird should get an upward speed,this.y = -8(the top is negative), because of the presence of the acceleration, the velocity will soon become downward, so the bird will eventually move downward

Step 9 Collision detection

  1. usegetBoundsOf the elementx.y.width.height, how to make cross judgment between the two elements, that is, whether the four points in the rectangular area of A are in the rectangular area of B
  2. We will see that the bird’s visual range contains some transparent parts, and using this rectangle directly will be a little inaccurate. We can rewrite the bird classgetBoundsMethod (remember to call the parent method first) and adjust the range to 1/2
  getBounds(. args) {
    const rect = AnimatedSprite.prototype.getBounds.call(this. args); rect.x = rect.x + rect.width /4;
    rect.y = rect.y + rect.height / 4;
    rect.width *= 0.5;
    rect.height *= 0.5;
    return rect;
  }
Copy the code

Step 10 Scoreboard

The scoreboard is wrapped in Container, converts the score to a string, and then iterates through the sprites for element creation

Write in the last

If you have the need to private letter me, welcome to give advice, thank you for reading