前言
且说上回,PixiJS游戏开发应用实践(一)分享了一些关PixiJs
的基础知识,封装了一个简单的资源预加载器,今天把后续的一些功能API
分享出来。
帧动画播放
帧动画是一种常用的动画实现形式,通过将动画的动作对象分解成多张连续的关键帧,然后连续播放从而实现动画。
AnimatedSprite对象
作为一个优秀的渲染框架,pixi当然也包含支持帧动画播放的API-AnimatedSprite
对象。它的官方类型说明文档如下:
export declare class AnimatedSprite extends Sprite {
animationSpeed: number; // 帧动画播放速度
loop: boolean; // 是否循环播放
updateAnchor: boolean; // 是否更新动画对应纹理的资源
onComplete?: () => void; // 播放结束回调(loop = false执行)
onFrameChange?: (currentFrame: number) => void; // 动画精灵纹理重新渲染时的回调
onLoop?: () => void; // loop = true时,循环播放时触发的回调
play(): void; // 播放方法
}
帧动画设计图的布局
帧动画,每一帧对应的宽高尺寸是完全一致的,帧数多的情况下可能会多行分布,下方就是一张2行*12列的烟花帧动画资源图,且第二行只有两帧,播放完这帧之后如果再播放就会出现空白动画,因此这一帧是它的最后播放帧。
API封装
首先我们先设计一下这个帧动画播放类需要传入的参数,列举如下:
interface animateParams {
/** 帧动画资源的纹理名称 */
name: string
/** 行数 */
rows: number
/** 列数 */
columns: number
/** 最后一行空白的帧数 */
cutnum?: number
/** 动画播放速度 */
speed?: number
/** 动画是否循环播放 */
loop?: boolean
/** 动画播放结束回调(loop为false才会触发) */
onComplete?: () => void
}
其中的name
就是我们上一回讲到的通过预加载器Preloader
,预加载的纹理对象名称,大概的思路就是读取其中的每一帧,创建一个单帧尺寸大小的矩形区域,然后用纹理Texture
对象加载。
export default class extends AnimatedSprite {
constructor(option: animateParams) {
const { name, columns, rows, cutnum = 0, speed = 1, loop = true, onComplete } = option
const texture = TextureCache[name]
// 单帧的宽高尺寸大小
const width = Math.floor(texture.width / columns)
const height = Math.floor(texture.height / rows)
// 帧动画播放资源组
const framesList = []
// 遍历帧动画图片源创建一个动画纹理数组
for (let i = 0; i < rows; i++) {
for (let j = 0; j < columns; j++) {
// 空白区域不绘制
if (cutnum && i === rows - 1 && j > cutnum) {
break
}
// 创建一帧的矩形区域
const recttangle = new Rectangle(j * width, i * height, width, height)
const frame = new Texture(texture.baseTexture, recttangle)
framesList.push(frame)
}
}
// 执行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('动画播放结束')
}
})
animateSprite.play()
调用方式也比较简单,创建一个播放实例对象,然后调用play
方法即可。
碰撞检测
碰撞检测也是游戏开发中常用的一种功能api。在2D环境下,常见的碰撞类型有以下几种:
- 轴对称包围盒(Axis-Aligned Bounding Box),即无旋转矩形
- 圆形碰撞
轴对称包围盒(双矩形)
- 概念:判断任意两个(无旋转)矩形的任意一边是否无间距,来判断是否碰撞 核心算法:
Math.abs(vx) < combinedHalfWidths && Math.abs(vy) < combinedHalfHeights
// vx, vy两个矩形中心点横、纵坐标的距离大小
// combinedHalfWidths、combinedHalfHeights为宽高一半之和的距离
算法缺点:
- 局限,两个物体必须是矩形,且不能旋转,即水平和垂直方向对称
- 对于一些包含图案(未填满整个矩形)的矩形进行碰撞检测,可能存在精度不足的问题
圆形碰撞
- 比较两个圆形圆心之间的距离和半径之和的大小 核心算法:
Math.sqrt(Math.pow(circleA.x - circleB.x, 2) + Math.pow(circleA.y - circleB.y, 2)) < circleA.radius + circleB.radius
算法缺点:同轴对称包围盒第二条类似。
事件代理
pixi有自己的一套事件处理系统,支持绘制元素上直接监听事件。但是在开发过程中,发现其中的tap
事件在跨端上有bug,即绑定在精灵对象上的tap
事件,用户在移动端真机上上下滑动也会触发该事件,导致用户体验非常差。因此针对这种情况,要么做用户touch
时长判断到底是click
还是scroll
事件,但是时间并不可控。因此需要对原有的基础事件类型做一层封装,加上一些支持跨端的联合事件类型。
原理
利用发布-订阅者设计模式,实现事件监听、触发事件回调的功能,这其中就要用到eventemitter3
这个特别经典的事件发送者
库。
export default class EventManager {
private static event = new EventEmitter() // 定义一个事件监听对象
// 定义一些pc+mobile联合事件类型
public static ALL_CLICK: string = 'tap+click'
public static ALL_START: string = 'touchstart+mousedown'
public static ALL_MOVE: string = 'touchmove+mousemove'
/**
* 事件监听-静态方法,不需要实例化调用,直接通过类来调用
* @param {String} name-事件名称
* @param {Function} fn-事件触发回调
* @param {Object} context-上下文对象
*/
public static on(
name: string,
fn: Function,
context?: any
) {
EventManager.event.on(name, <any>fn, context)
}
/**
* 事件触发
* @param {String} name-事件名称
*/
public static emit(name: string, ...args: Array<any>) {
EventManager.event.emit(name, ...args)
}
}
定义完上面的事件管理器后,我们知道pixi的继承关系:DisplayObject > Container > Sprite
,因此我们需要直接代理DisplayObject
-显示对象原型上的on
和of
方法,通过重写原型上的方法:
const on = PIXI.Container.prototype.on
const off = PIXI.Container.prototype.off
/**
* 代理on监听器
*/
function patchedOn<T extends PIXI.DisplayObject>(
event: string,
fn: Function
): T {
// 针对联合事件单独处理
switch (event) {
case EventManager.ALL_CLICK:
// tap+click联合事件
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监听器原理一样
// 重新反向赋值
PIXI.Container.prototype.on = patchedOn
PIXI.Container.prototype.off = patchedOff
使用方式也很简单,需要将下面的代理模块引入,然后直接调用EventManager
模块的方法:
// 初始化事件监听器
EventManager.on('circle', (data: unknown) => {
console.log(data)
})
circle.on(EventManager.ALL_CLICK, (e) => {
// 触发事件
EventManager.emit('circle', e.type)
})
// 监听`ALL_CLICK`联合事件就不会出现滑动触发`tap`事件的bug
后记
本次是这个系列的终篇,针对PixiJs
做了一个比较深入的应用实践总结,分享给大家,希望有所帮助。后续针对游戏开发这个方向,会继续分享更多实用的内容,大家一起进步,有问题评论区码字,也欢迎指错勘误!
参考
2. pixijs官方文档