Sprite类目前没有实现动画支持,所以有一个新类Animation,继承自Sprite,额外提供了动画功能。所谓动画功能,实际上就是根据每一帧调用,更换图片,动画本质就是一连串图片的播放。
Animation.js
代码如下,
export default class Animation extends Sprite {
constructor(imgSrc, width, height) {
super(imgSrc, width, height)
// 当前动画是否播放中
this.isPlaying = false
// 动画是否需要循环播放
this.loop = false
// 每一帧的时间间隔
this.interval = 1000 / 60
// 帧定时器,这里采用
this[__.timer] = null
// 当前播放的帧
this.index = -1
// 总帧数
this.count = 0
// 帧图片集合
this.imgList = []
/**
* 推入到全局动画池里面
* 便于全局绘图的时候遍历和绘制当前动画帧
*/
databus.animations.push(this)
}
initFrames函数
在创建enemy对象时会被调用,这个游戏里面动画效果只有敌机被击落时才发生。
/**
* 初始化帧动画的所有帧
* 为了简单,只支持一个帧动画
*/
initFrames(imgList) {
imgList.forEach((imgSrc) => {
const img = new Image()
img.src = imgSrc
this.imgList.push(img)
})
this.count = imgList.length
}
playAnimation函数
负责调用触发动画的准备工作,比如让该对象显示的静态敌机图片无需渲染,后续渲染都是渲染爆炸效果的图片,而爆炸图片的变化又有这里启动了一个定时器来控制。
// 播放预定的帧动画
playAnimation(index = 0, loop = false) {
// 动画播放的时候精灵图不再展示,播放帧动画的具体帧
this.visible = false
this.isPlaying = true
this.loop = loop
this.index = index
if (this.interval > 0 && this.count) {
this[__.timer] = setInterval(
this.frameLoop.bind(this),
this.interval
)
}
}
aniRender函数
渲染动画的方法为aniRender函数,还记得渲染精灵的图片方法么?是Sprite.drawToCanvas。因为visiable=false,所以精灵渲染就会消失,只剩动画渲染。
// 将播放中的帧绘制到canvas上
aniRender(ctx) {
ctx.drawImage(
this.imgList[this.index],
this.x,
this.y,
this.width * 1.2,
this.height * 1.2
)
}
frameLoop函数
动画效果里面提到一个定时器,setInterval调用的是frameLoop方法,功能简单,就是改变动画图片的索引值index,当达到最后一张时,如果设置的是循环动画播放则重置为第一张,否则终止定时器。
// 帧遍历
frameLoop() {
this.index++
if (this.index > this.count - 1) {
if (this.loop) {
this.index = 0
} else {
this.index--
this.stop()
}
}
}
在这个项目中都是非循环动画,所以可以正常工作,但是如果是循环动画,定时器会在对象回收到pool之后,还一直调用,因为并没有其他调用的地方来判断何时停止定时器,所以完善的话,应该在回收时注意清理。
我觉得实际上这个函数应该由update函数调用,因为update函数是有全局的定时器控制的,时间间隔也一样,不用再额外创建定时器了。
Enemy.js
enemy继承了Animation。飞机需要速度,所以需要设置速度,飞机的移动时基于速度来计算坐标位置。初始化动画爆炸图片,接下来所有功能基本就靠Animation来实现了,无需自己实现。
这也是这个游戏中唯一用到动画功能的地方,其他功能都只是移动。因为基本功能基本都靠Animation和Sprite实现,所以,在具体敌人飞机上只要调用相关功能即可。
import Animation from '../base/animation'
import DataBus from '../databus'
const ENEMY_IMG_SRC = 'images/enemy.png'
const ENEMY_WIDTH = 60
const ENEMY_HEIGHT = 60
const __ = {
speed: Symbol('speed')
}
const databus = new DataBus()
function rnd(start, end) {
return Math.floor(Math.random() * (end - start) + start)
}
export default class Enemy extends Animation {
constructor() {
super(ENEMY_IMG_SRC, ENEMY_WIDTH, ENEMY_HEIGHT)
this.initExplosionAnimation()
}
init(speed) {
this.x = rnd(0, window.innerWidth - ENEMY_WIDTH)
this.y = -this.height
this[__.speed] = speed
this.visible = true
}
// 预定义爆炸的帧动画
initExplosionAnimation() {
const frames = []
const EXPLO_IMG_PREFIX = 'images/explosion'
const EXPLO_FRAME_COUNT = 19
for (let i = 0; i < EXPLO_FRAME_COUNT; i++) {
frames.push(`${EXPLO_IMG_PREFIX + (i + 1)}.png`)
}
this.initFrames(frames)
}
// 每一帧更新子弹位置
update() {
this.y += this[__.speed]
// 对象回收
if (this.y > window.innerHeight + this.height) databus.removeEnemey(this)
}
}