mydal

怕光阴流逝 让一切都作废

0%

微信小游戏-飞机大战代码分析

​ 最近要开发一个微信小游戏,本人在这方面是一窍不通,先找了微信小游戏官方的一个demo,学习一下.

目录分析

README.md中有详细的介绍,我们来看一下.

这里其实漏掉了一个game.js. game.js是小游戏的入口文件. 他实例化了Main()

1
2
3
4
5
6
7
//game.js
import './js/libs/weapp-adapter'
import './js/libs/symbol'

import Main from './js/main'

new Main()

接下来,我们来分析飞机大战的主要文件.

main.js

​ main是游戏主函数.是这个游戏的核心.在分析main.js之前,我们先来了解一些基础知识.

基础知识

  • 帧:在视频领域,电影电视数字视频等可视为随时间连续变换的许多张画面,其中是指每一张画面。
  • 帧数:帧数(Frames),为帧生成数量的简称。由于口语习惯上的原因,我们通常将帧数与帧率混淆。每一帧都是静止的图象,快速连续地显示帧便形成了运动的假象,因此高的帧率可以得到更流畅、更逼真的动画。其实,我们所看的动画,就是所谓的动态画面,电影,电视,动漫,就是一幅幅画面按照顺序叠加起来的,像50帧就是一秒50帧,代表一秒钟会有50张画面.
  • 精灵:是游戏中的一个基本概念,指的是在游戏中的一个基本物体或动画或贴图,如NPC或者敌人,在本游戏中有子弹,敌机和玩家
  • 回调函数:在特定事件发生后,由事件方进行调用的函数
  • 画布:顾名思义就是使用了画东西的地方,其实就是用于渲染相关内容的位置

​ 接下来,我们看一下main.js中各个方法,代码中夹杂了我自己写的一些注释,帮助我们更好的理解.

contructor()和restart()

​ contructior()是一个构造函数,用于创建main对象的,他里面调用了restart方法,我们着重看一下restart方法.

restart主要负责生产一个界面,初始化各类信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
restart() {
databus.reset(); //全局状态管理器
canvas.removeEventListener(
// 方法用于移除由 addEventListener() 方法添加的事件句柄
"touchstart",
this.touchHandler
);

this.bg = new BackGround(ctx); //初始化背景
this.player = new Player(ctx); //初始化玩家
this.gameinfo = new GameInfo(); //初始化游戏信息
this.music = new Music(); //初始化音乐

this.bindLoop = this.loop.bind(this); //绑定事件循环
this.hasEventBind = false; //初始化状态

// 清除上一局的动画
window.cancelAnimationFrame(this.aniId);
//取消一个先前通过调用window.requestAnimationFrame()方法添加到计划中的动画帧请求。

this.aniId = window.requestAnimationFrame(
//告诉浏览器——你希望执行一个动画
this.bindLoop,
canvas
);
}

enemyGenerate()

​ enemyGenerate()作用就是生成敌机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 随着帧数变化的敌机生成逻辑
* 帧数取模定义成生成的频率
*/
enemyGenerate() {
//databus.frame作用相当于每次刷新的计数器,当我们使用它时会做出判断,
//当刷新次数为30的整数倍时(30余0),就会生成一个新的敌机对象并将其初始化,
//其中init的参数为该敌机的速度,生成后加入databus对象的存储数组中

if (databus.frame % 30 === 0) {
const enemy = databus.pool.getItemByClass("enemy", Enemy);
enemy.init(8);
databus.enemys.push(enemy);
}
}

// 全局碰撞检测
collisionDetection() {
// const that = this
//首先对于每一个子弹,要判断子弹是否与敌机相撞,如果相撞的话则就会隐藏敌人的飞机和子弹,我们明白,
//子弹撞击到飞机说明子弹和飞机都爆炸摧毁了,
//但这不代表在逻辑中将对象删除,而是在画面中它消失了,这时就是判断visible是否为true,如果为true的就不画到画布上,
//而统一更新回收入pool 对每一架敌机,判断是否与用户相撞,若相撞,则在databus中设置游戏结束。

databus.bullets.forEach((bullet) => {
for (let i = 0, il = databus.enemys.length; i < il; i++) {
const enemy = databus.enemys[i];

if (!enemy.isPlaying && enemy.isCollideWith(bullet)) {
enemy.playAnimation();
this.music.playExplosion();

bullet.visible = false;
databus.score += 1;

break;
}
}
});

for (let i = 0, il = databus.enemys.length; i < il; i++) {
const enemy = databus.enemys[i];

if (this.player.isCollideWith(enemy)) {
databus.gameOver = true;

break;
}
}
}

touchEventHandler()

游戏结束后判断是否重新开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 游戏结束后的触摸事件处理逻辑
touchEventHandler(e) {
//获取触摸的坐标,在gameinfo中获取重新开始上下左右xy坐标,比对触摸位置是否在按钮内部,若在则调用restart函数重新启动函数。
e.preventDefault();

const x = e.touches[0].clientX;
const y = e.touches[0].clientY;

const area = this.gameinfo.btnArea;

if (
x >= area.startX &&
x <= area.endX &&
y >= area.startY &&
y <= area.endY
)
this.restart();
}

render()

render()是用于渲染场景,用于每次修改内容后重新渲染场景内容(每一帧调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* canvas重绘函数
* 每一帧重新绘制所有的需要展示的元素
*/
render() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 把像素设置为透明以达到一个圆形区域的目的
//在一个椭圆区域内设置所有像素都是透明的(rgba(0,0,0,0))。这个范围椭圆的左上角在 (x, y),宽度和高度分别由 width和height确定

this.bg.render(ctx); //调用背景类的渲染函数 渲染背景

// 连接接databus中的bullets和enemys数组,并且将这个合成数组中的每一项画到画布上,
// 画到画布上的操作是以利用函数drawToCanvas,而该函数实现于Spirite类中
//spirit即精灵,是游戏设计中的一个概念,相当于游戏中一个最基本的物体或者一个概念
databus.bullets.concat(databus.enemys).forEach((item) => {
item.drawToCanvas(ctx);
});

//将player画到画布上,同样的,player也继承于Spirit类,将所有动画类的未播放的内容进行播放
this.player.drawToCanvas(ctx);

databus.animations.forEach((ani) => {
if (ani.isPlaying) {
ani.aniRender(ctx);
}
});

//Animation类继承Spirit,而所有物体均继承于Animation类,
//不过由于所有物体都均仅有一帧图像,因此无需进行播放,在databus类中有一个专门存放动画的数组,
//任何继承于Animation类的对象都会在初始化构造时被放入该数组当中,调用gameinfo的函数更新图像左上角的分数内容判断,
//若游戏结束 若未绑定事件,将touchHandler事件添加绑定, 将事件加入监听中。

this.gameinfo.renderGameScore(ctx, databus.score);

// 游戏结束停止帧循环
if (databus.gameOver) {
this.gameinfo.renderGameOver(ctx, databus.score);

if (!this.hasEventBind) {
this.hasEventBind = true;
this.touchHandler = this.touchEventHandler.bind(this);
canvas.addEventListener("touchstart", this.touchHandler); //addEventListener方法用于向指定元素添加事件句柄
}
}
}

update()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 游戏逻辑更新主函数
//update()是逻辑更新主函数,若游戏已经结束,不执行该代码,
//直接放回结束,更新背景参数,对所有bullets和enemys对象进行更新,
//调用enemyGenerate() 生成敌人(根据前面描述,需要判断是否满足刚好经过30帧) 进行全局碰撞检测,并进行处理,
//判断是否经过20帧,每经过20帧,调用player生成一个新的bullet(子弹),并且调用射击音乐 loop()。
update() {
if (databus.gameOver) return;

this.bg.update();

databus.bullets.concat(databus.enemys).forEach((item) => {
item.update();
});

this.enemyGenerate();

this.collisionDetection();

if (databus.frame % 20 === 0) {
this.player.shoot();
this.music.playShoot();
}
}

loop()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实现游戏帧循环
//实现游戏帧循环,每次循环将帧计数器加一,更新逻辑,渲染逻辑更新后的场景,
//使用window.requestAnimationFrame进行调用,为下一帧界面渲染做准备。
loop() {
databus.frame++;

this.update();
this.render();

this.aniId = window.requestAnimationFrame(
//告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画
this.bindLoop,
canvas
);
}

​ 这就是main.js的主要内容,接下来我们看一下databus.js

databus.js

​ databus.js是一个全局状态管理器.databus的作用是检测全局状态,回收敌机和子弹,那么什么是回收?什么是对象池?

首先我们要知道敌机或是子弹这种频繁出现的东西,虽然都是基于同一个对象,但每一个物体在使用的时候都需要单独开辟空间,如果使用后不回收的话就会非常浪费空间,而这时我们就会用到池,这个池简单来说就是一个容器,用来存放子弹或敌机对象,当我们需要一个子弹或是一个敌机时,就从池中取,位置速度重新初始化一下就可以了。不需要了,再放回池中。

constructor()

我们来看一下 constructor()做了什么.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* constructor构造器,如果instance不为空已经存在,那么就返回instance,这是实现单例模式,
* 保证不管多少次new都只能产生一个对象,如果不为空,将instance设置为自身,并进行下列初始化操作,
* 创建一个对象池pool,对象池技术是通过将生成的对象暂时保存于池中,需要对象时先在池中查看是否有多余对象,
* 若不足再生成对象,而在销毁对象时不进行真正销毁,而是加入对象池中,重置所有内容,设置为空。
*
*/
constructor() {
//instance用于承载该文件中惟一的databus类,实现单例模式,
//单例模式是一种设计模式,保证全局仅有一个该类的对象,这样能在该demo中保证全局数据的一致性。
if (instance) return instance

instance = this

this.pool = new Pool()

this.reset()
}

reset()

1
2
3
4
5
6
7
8
reset() {  //重置所有内容
this.frame = 0
this.score = 0
this.bullets = []
this.enemys = []
this.animations = []
this.gameOver = false
}

removeEnemey()

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 回收敌机,进入对象池
* 此后不进入帧循环
* 移除某个敌方对象(敌机)。从enemys数组中获取第一个元素,
* shift方法是js中移除第一个元素并返回的方法,设置其不可见,移入名为enemy的池中
*/
removeEnemey(enemy) {
const temp = this.enemys.shift()

temp.visible = false

this.pool.recover('enemy', enemy) //将对象回收到对象池
}

removeBullets()

1
2
3
4
5
6
7
8
9
10
11
/**
* 回收子弹,进入对象池
* 此后不进入帧循环
*/
removeBullets(bullet) {
const temp = this.bullets.shift()

temp.visible = false

this.pool.recover('bullet', bullet)
}

​ 现在,我们一起来看一下敌机类(enemy.js)

enemy.js

​ enemy用于生成敌机。它继承了Animation类,引入了databus,实例化了databus

rnd()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Symbol([description])  es6新增 作为对象属性的唯一标识符,防止对象属性冲突发生。 Symbol()返回值是唯一的 
//Symbol("foo") === Symbol("foo"); // false 唯一的
//symbol 基本数据类型
//description,可选的,字符串类型。对符号的描述,可用于调试但不是访问符号服务。
const __ = {
speed: Symbol("speed"),
};

const databus = new DataBus();

function rnd(start, end) {
//Math.random() 用于提供[0,1)区间的浮点数
//Math.floor() 返回小于等于该数字最大的整数
//该函数的作用是返回一个start到end区间(end不取)返回内的一个随机数
//用于生成敌机的位置
return Math.floor(Math.random() * (end - start) + start);
}

constructor()

1
2
3
4
5
6
7
constructor() {
//根据提供的常量初始化敌机对象
//并且初始化爆炸动画,该函数在之后实现
super(ENEMY_IMG_SRC, ENEMY_WIDTH, ENEMY_HEIGHT);

this.initExplosionAnimation();
}

init()

1
2
3
4
5
6
7
8
9
10
11
12
//初始化敌机速度
init(speed) {
//获取随机生成x坐标作为起始x位置
this.x = rnd(0, window.innerWidth - ENEMY_WIDTH); // innerWidth返回以像素为单位的内部宽度
//获取其本身的高度取负值作为起始y坐标(一开始整个敌机还未进入屏幕,慢慢一点一点进入)
//js中坐标原点为屏幕左上角,以原点向左为x正方向,原点向下为y正方向,
this.y = -this.height;

this[__.speed] = speed;

this.visible = true;
}

initExplosionAnimation()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 预定义爆炸的帧动画
/**
* 创建一个数组
* 设定爆炸的每一帧动画的具体位置,以及数量
* 创建一个frames数组,将图片按顺序读取并加入数组中
* 将该数组作为Animation类中定义的方法initFrames的参数初始化爆炸动画
*/
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()

1
2
3
4
5
6
7
8
9
// 每一帧更新子弹位置
//逻辑更新函数,更新物体的参数,基本每个具体物体都具有该函数
//按速度没回合加上一定的y坐标(由于敌机是往下走的,因此加上)
// 若发现对象移动出屏幕,则将其回收
update() {
this.y += this[__.speed];
// 对象回收
if (this.y > window.innerHeight + this.height) databus.removeEnemey(this);
}

再来瞅瞅我们的子弹类(bullet.js)

bullet.js

​ bullet继承了sprite(精灵类),实例化了databus(). bullet.js代码比较少,我直接贴上来,来分析一下.重点看update方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Sprite from "../base/sprite";
import DataBus from "../databus";
//定义一些常量
const BULLET_IMG_SRC = "images/bullet.png";
const BULLET_WIDTH = 16;
const BULLET_HEIGHT = 30;

const __ = {
speed: Symbol("speed"),
};

const databus = new DataBus();

export default class Bullet extends Sprite {
constructor() {
super(BULLET_IMG_SRC, BULLET_WIDTH, BULLET_HEIGHT);
}
// 初始化坐标位置和速度
init(x, y, speed) {
this.x = x;
this.y = y;

this[__.speed] = speed;

this.visible = true;
}

// 每一帧更新子弹位置
//为y坐标向上增加速度的大小,即应该减去速度的数值
// 将整个子弹超出屏幕外的(因此是小于-this.height而不是0)移入对象池中
update() {
this.y -= this[__.speed];

// 超出屏幕外回收自身
if (this.y < -this.height) databus.removeBullets(this);
}
}

看一下我们的玩家类(index.js)

index.js

​ player 同样继承了sprite,实例化了databus.敌机,子弹,玩家,都用到了databus中的一些属性或方法.

constructor()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor() {
super(PLAYER_IMG_SRC, PLAYER_WIDTH, PLAYER_HEIGHT);

// 玩家默认处于屏幕底部居中位置
this.x = screenWidth / 2 - this.width / 2;
this.y = screenHeight - this.height - 30;

// 用于在手指移动的时候标识手指是否已经在飞机上了
this.touched = false;

this.bullets = [];

// 初始化事件监听
this.initEvent(); //事件监听相当于是在等待事件的发生,一旦发生就会随之执行的函数
}

checkIsFingerOnAir()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 当手指触摸屏幕的时候
* 判断手指是否在飞机上
* @param {Number} x: 手指的X轴坐标
* @param {Number} y: 手指的Y轴坐标
* @return {Boolean}: 用于标识手指是否在飞机上的布尔值
*/
checkIsFingerOnAir(x, y) {
const deviation = 30;

return !!(
x >= this.x - deviation &&
y >= this.y - deviation &&
x <= this.x + this.width + deviation &&
y <= this.y + this.height + deviation
);
}

setAirPosAcrossFingerPosZ()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 根据手指的位置设置飞机的位置
* 保证手指处于飞机中间
* 同时限定飞机的活动范围限制在屏幕中
*/
setAirPosAcrossFingerPosZ(x, y) {
let disX = x - this.width / 2;
let disY = y - this.height / 2;

if (disX < 0) disX = 0;
else if (disX > screenWidth - this.width) disX = screenWidth - this.width;

if (disY <= 0) disY = 0;
else if (disY > screenHeight - this.height)
disY = screenHeight - this.height;

this.x = disX;
this.y = disY;
}

initEvent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 玩家响应手指的触摸事件
* 改变战机的位置
*/
initEvent() {
//绑定touchstart事件, 即开始触碰事件,并传入一个匿名函数作为回调函数,作为触发该事件时的回调
canvas.addEventListener("touchstart", (e) => {
e.preventDefault(); //取消事件本身的默认动作的函数

const x = e.touches[0].clientX;
const y = e.touches[0].clientY;

//若触碰时触碰的是飞机则将飞机被触碰设置为真并且将飞机中心移动到手指中心
if (this.checkIsFingerOnAir(x, y)) {
this.touched = true;

this.setAirPosAcrossFingerPosZ(x, y);
}
});

//绑定touchmove事件,即触碰移动
canvas.addEventListener("touchmove", (e) => {
e.preventDefault();

const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
//若触碰飞机情况为真,则将飞机移动到相应位置
if (this.touched) this.setAirPosAcrossFingerPosZ(x, y);
});

//绑定touchend事件,即触碰结束
canvas.addEventListener("touchend", (e) => {
e.preventDefault();

this.touched = false;
});
}

shoot()

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 玩家射击操作
* 射击时机由外部决定
*/
shoot() {
//从对象池中取一个子弹
// 根据玩家位置初始化子弹位置
const bullet = databus.pool.getItemByClass("bullet", Bullet);

bullet.init(this.x + this.width / 2 - bullet.width / 2, this.y - 10, 10);

databus.bullets.push(bullet);
}

接下来,看一下很重要的动画类(Animation)

Animation.js

​ animation.js就是所有动画所在的文件,他继承了spirit类,我们来看看他具体做了什么.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Sprite from './sprite'
import DataBus from '../databus'

const databus = new DataBus()

const __ = {
//Symbol([description]) es6新增 作为对象属性的唯一标识符,防止对象属性冲突发生。 Symbol()返回值是唯一的
//Symbol("foo") === Symbol("foo"); // false 唯一的
//symbol 基本数据类型
//description,可选的,字符串类型。对符号的描述,可用于调试但不是访问符号服务。

timer: Symbol('timer'),
}

/**
* 简易的帧动画类实现
* animation.js就是所有动画所在的文件
* 引入Spirit类和DataBus类,生成一个databus对象,确定一个Symbol对象
* 继承Spirit类
*/

constructor()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//constructor 作用是先用图片路径和宽度高度初始化超类spirit类
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 初始化帧动画的所有帧
* 为了简单,只支持一个帧动画
*/
initFrames(imgList) {
console.log(imgList);

imgList.forEach((imgSrc) => {
const img = new Image()
img.src = imgSrc

this.imgList.push(img)
})

this.count = imgList.length
}

aniRender()

1
2
3
4
5
6
7
8
9
10
11
// 将播放中的帧绘制到canvas上
aniRender(ctx) {
//调用drawImage画上动画在该时刻应该有的图像
ctx.drawImage(
this.imgList[this.index],
this.x,
this.y,
this.width * 1.2,
this.height * 1.2
)
}

playAnimation()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 播放预定的帧动画
//将精灵图的可见设为false,在本例子中有一个敌机被击毁,发生了敌机爆炸,展示爆炸的动画,设置正在播放,
//将是否循环的情况设置为初始设置的,初始设置为不循环,判断是否有动画切换间隔和帧数,有的话设置定时器,使用函数setInterval。
//setInterval函数,第一个参数是回调函数,是在这个过程中不断调用的函数,第二个参数是间隔,
//整个函数的含义就是在该间隔内不断调用传入的回调函数,
//猜测是一般情况来说主函数中的图像切换频率大于该间隔,这样才能体现动画的变化
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
)
}
}

stop()

1
2
3
4
5
6
7
// 停止帧动画播放
//将播放设置为false,清除原本设置的定时器。
stop() {
this.isPlaying = false

if (this[__.timer]) clearInterval(this[__.timer])
}

frameLoop()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 帧遍历
//帧计数变量index加加,
//若帧数大于图片数-1,由于计数从0开始,如果要求循环,将index置0,否则将index--,即设置为最后一张图片,并且调用stop()函数暂停。
frameLoop() {
this.index++

if (this.index > this.count - 1) {
if (this.loop) {
this.index = 0
} else {
this.index--
this.stop()
}
}
}

​ 回顾一下,这几个文件都继承或引入了sprite,那我们就来看看这个精灵类,它具体做了哪些事情.

sprite.js

代码不多,我直接贴上来,代码中同样夹杂了我自己的一些注释.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 游戏基础的精灵类
*/
export default class Sprite {
constructor(imgSrc = '', width = 0, height = 0, x = 0, y = 0) { //生成一个精灵 x,y指图片的左上角坐标
this.img = new Image()
this.img.src = imgSrc

this.width = width
this.height = height

this.x = x
this.y = y

this.visible = true
}

/**
* 将精灵图绘制在canvas上
*/
drawToCanvas(ctx) {
if (!this.visible) return //不可见 则画不到画布上

ctx.drawImage(
this.img,
this.x,
this.y,
this.width,
this.height
)
}

/**
* 简单的碰撞检测定义:
* 另一个精灵的中心点处于本精灵所在的矩形内即可
* @param{Sprite} sp: Sptite的实例
*/
isCollideWith(sp) {
//根据传入物体的左上角的坐标和大小计算中心坐标
const spX = sp.x + sp.width / 2
const spY = sp.y + sp.height / 2
//如果两个物体中任意一个不可见 则无需计算
if (!this.visible || !sp.visible) return false

//判断传入物体的中心坐标 是否在该物体的方框之内
return !!(spX >= this.x
&& spX <= this.x + this.width
&& spY >= this.y
&& spY <= this.y + this.height)
}
}

​ 最后,我们来看一下,pool.js(对象池的简易实现)

pool.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//用于防止魔术字符串出现的常量列表
//symbol类型为类似于字符串的类型,不能使用new命令,也不能添加属性,在这里声明了一个对象名称为poolDic,用于保存多个对象池的一个字典。
const __ = {
poolDic: Symbol('poolDic')
}

/**
* 简易的对象池实现
* 用于对象的存贮和重复使用
* 可以有效减少对象创建开销和避免频繁的垃圾回收
* 提高游戏性能
*/
export default class Pool {
constructor() {
this[__.poolDic] = {}
}

/**
* 根据对象标识符
* 获取对应的对象池
*/
getPoolBySign(name) {
return this[__.poolDic][name] || (this[__.poolDic][name] = [])
}

/**
* 根据传入的对象标识符,查询对象池
* 对象池为空创建新的类,否则从对象池中取
* 获取对象,获取相应对象池,判断对象池是否为空,若不为空,返回第一个元素,并从对象池中移除,若为空,则用传入的类名生成新的一个对象。
*/
getItemByClass(name, className) {
const pool = this.getPoolBySign(name)

const result = (pool.length
? pool.shift()
: new className())

return result
}

/**
* 将对象回收到对象池
* 方便后续继续使用
*/
recover(name, instance) {
this.getPoolBySign(name).push(instance)
}
}

总结

​ 整个demo看下来,首先,小游戏就是一个canvas画布.这个游戏,主体就是敌机,子弹和玩家三类 实际上,他们都是一个单独的精灵,在代码中也继承了精灵类,最终才能绘制在canvas上.敌机类不太一样,因为他有一个爆炸效果,所以继承了动画类,做了一个爆炸的帧动画.我们的main游戏主函数,他是主体,它初始化了背景,玩家,游戏信息,音乐等信息,定义了全局的碰撞检测,实现了游戏的帧循环.而databus则是管理全局的状态,包括敌机,子弹,玩家,动画,分数等。对子弹和敌机做了回收操作.

​ 整体调试了几遍代码,在网上也看了相关的分析,算是初步了解了微信小游戏,但对这个demo的理解还是不够深,对一些操作也有点疑问,对微信小游戏感知还是比较模糊.不过,箭在弦上,不得不发了.祝我好运!!!!