preface

Now, last time, PixiJS game development application practice (a) to share some of the basic knowledge of PixiJS, packaged a simple resource preloader, today to share some of the subsequent functions of the API.

Frame animation playback

Frame animation is a common form of animation realization, which is achieved by decomposing the animation action object into several consecutive key frames and then playing them continuously.

AnimatedSprite object

As an excellent rendering framework, Pixi also includes an API-AnimatedSprite object that supports frame animation. Its official type description documentation is as follows:

export declare class AnimatedSprite extends Sprite {
    animationSpeed: number; // Frame animation playback speed
    loop: boolean; // Whether to loop
    updateAnchor: boolean; // Whether to update the resources of the animation textureonComplete? :() = > void; // Play end callback (loop = false)onFrameChange? :(currentFrame: number) = > void; // Animation Sprite texture callback when re-renderingonLoop? :() = > void; // callback triggered when loop = true
    
    play(): void; // Play method
}
Copy the code

Frame the layout of the animation design

Frame animation, the width and height of each frame is exactly the same, in the case of a large number of frames, there may be multiple lines of distribution, below is a 2 *12 column fireworks frame animation resource map, and the second line only has two frames, after playing this frame, if you play it again, there will be a blank animation, so this frame is its last playing frame.

API package

First of all, let’s design the parameters that need to be passed into the frame animation playback class, listed as follows:

interface animateParams {
  /** The texture name of the frame animation resource */
  name: string
  / line number * * * /
  rows: number
  The column number / * * * /
  columns: number
  /** The number of blank frames in the last line */cutnum? :number
  /** Animation playback speed */speed? :number
  /** Whether the animation plays in a loop */loop? :boolean
  /** Animation end callback (loop is false) */onComplete? :() = > void
}
Copy the code

The name is the name of the Texture object preloaded by the Preloader. The general idea is to read each frame, create a single frame size rectangle, and then load it with the Texture object.

export default class extends AnimatedSprite {
  constructor(option: animateParams) {
    const { name, columns, rows, cutnum = 0, speed = 1, loop = true, onComplete } = option
    const texture = TextureCache[name]
    // The width and height of a single frame
    const width = Math.floor(texture.width / columns)
    const height = Math.floor(texture.height / rows)
    // Frame animation playback resource group
    const framesList = []
    // Create an animation texture array by traversing the frame animation source
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        // Blank areas are not drawn
        if (cutnum && i === rows - 1 && j > cutnum) {
          break
        }
        // Create a rectangle area for a frame
        const recttangle = new Rectangle(j * width, i * height, width, height)
        const frame = new Texture(texture.baseTexture, recttangle)
        framesList.push(frame)
      }
    }
    // Perform playback initialization for the parent AnimatedSprite
    super(framesList)
    this.animationSpeed = speed
    this.loop = loop
    this.onComplete = onComplete
  }
}

const animateSprite = new CreateMovieClip({
  name: 'fire'.rows: 2.columns: 12.cutnum: 10.speed: 0.5.loop: true.onComplete: () = > {
    console.log('Animation ends')
  }
})
animateSprite.play()
Copy the code

Call method is also relatively simple, create a play instance object, and then call the Play method.

Collision detection

Collision detection is also a feature API commonly used in game development. There are several common collision types in 2D:

  1. Axis-aligned Bounding boxes, which are non-rotating rectangles
  2. Circular collision

Axisymmetric bounding box (double rectangle)

  • Concept: Determine if any two (non-rotating) rectangles have no space on either side to determine whether they collide

Core algorithm:

Math.abs(vx) < combinedHalfWidths && Math.abs(vy) < combinedHalfHeights
// The distance between the center points of the rectangles vx and vy
// combinedHalfWidths and combinedHalfHeights are the sum of half of the width and height
Copy the code

Disadvantages of the algorithm:

  1. Limitations: The two objects must be rectangular and cannot rotate, i.e. symmetrical horizontally and vertically
  2. Collision detection for some rectangles that contain patterns (that do not fill the entire rectangle) may be inaccurate

Circular collision

  • Compare the sum of the distances and radii between the centers of two circles

Core algorithm:

Math.sqrt(Math.pow(circleA.x - circleB.x, 2) + Math.pow(circleA.y - circleB.y, 2)) < circleA.radius + circleB.radius
Copy the code

Algorithm disadvantage: the second axisymmetric bounding box is similar.

The event agent

Pixi has its own event handling system that allows you to listen for events directly on drawn elements. However, in the process of development, it was found that tap events were buggy on cross-ends, that is, tap events bound to Sprite objects would also be triggered when users swiped up and down on the real phone on the mobile end, resulting in poor user experience. Therefore, in this case, either the user touch duration to determine whether the event is click or Scroll, but the time is not controllable. Therefore, it is necessary to encapsulate the original base event types and add some federated event types that support cross-end.

The principle of

Eventemitter3 is a classic event sender library that uses the publish-subscriber design pattern to listen for events and trigger event callbacks.

export default class EventManager {
    private static event = new EventEmitter() // Define an event listener
    // Define some PC +mobile syndication event types
    public static ALL_CLICK: string = 'tap+click'
    public static ALL_START: string = 'touchstart+mousedown'
    public static ALL_MOVE: string = 'touchmove+mousemove'
    
    /** * event listener - static method that calls * directly from the class without instantiating the call@param {String} name- Event name *@param {Function} fn- Event triggers callback *@param {Object} context- Context object */
    public static on(
     name: string,
     fn: Function, context? :any
    ) {
     EventManager.event.on(name, <any>fn, context)
    }
    
    /** * Event trigger *@param {String} name- Event name */
    public static emit(name: string. args:Array<any>){ EventManager.event.emit(name, ... args) } }Copy the code

After defining the event manager above, we know the inheritance relationship of pixi: DisplayObject > Container > Sprite, so we need to directly delegate the on and of methods on the DisplayObject- DisplayObject prototype by overwriting the methods on the prototype:

const on = PIXI.Container.prototype.on
const off = PIXI.Container.prototype.off

/** * proxy on listener */
function patchedOn<T extends PIXI.DisplayObject> (
  event: string,
  fn: Function
) :T {
  // Handle the joint event separately
  switch (event) {
    case EventManager.ALL_CLICK:
      // tap+click combines events
      on.call(this, EventManager.TOUCH_CLICK, fn)
      return on.call(this, EventManager.MOUSE_CLICK, fn)
    case EventManager.ALL_START:
      on.call(this, EventManager.TOUCH_START, fn)
      return on.call(this, EventManager.MOUSE_DOWN, fn)
    case EventManager.ALL_MOVE:
      on.call(this, EventManager.TOUCH_MOVE, fn)
      return on.call(this, EventManager.MOUSE_MOVE, fn)
  }
  return on.apply(this.arguments)}// Off listeners work the same way
// reverse assignment again
PIXI.Container.prototype.on = patchedOn
PIXI.Container.prototype.off = patchedOff
Copy the code

It is also simple to use by introducing the following proxy module and calling the methods of the EventManager module directly:

// Initialize the event listener
EventManager.on('circle'.(data: unknown) = > {
    console.log(data)
})
circle.on(EventManager.ALL_CLICK, (e) = > {
    // Trigger the event
    EventManager.emit('circle', e.type)
})
// Listen for the 'ALL_CLICK' union event and there will be no sliding 'tap' event bug
Copy the code

Afterword.

This is the final chapter of this series, for PixiJs to do a more in-depth application practice summary, share with you, hope to help. Follow-up for game development this direction, will continue to share more practical content, we progress together, there are questions in the comment area code words, also welcome to point to the wrong errata!

reference

1. “Wait, I’ll touch!” — Common 2D collision detection

2. Pixijs official document