上期我们只实现了小鸟飞行以及障碍物地图随机生成、绘制和滚动,这期我们要完成整个游戏的大体框架,即游戏运行的主体部分,而其他的比如计分,重新开始等不会去实现。文章最后会讲一些个人心得。
本期内容依旧是在微信小游戏上进行实现的。由于内容以及代码都承接以前文章,如果你没有阅读过,可以从这里开始。
本文不允许任何形式的转载!
阅读提示
本系列文章不适合以下人群阅读,如果你无意点开此文,请对号入座,以免浪费你宝贵的时间。
- 想要学习利用游戏引擎开发游戏的朋友。本文不会涉及任何第三方游戏引擎。
- 不具备面向对象编程经验的朋友。本文的语言主要是Javascript(ECMA 2016),如果你不具备JS编程经验倒也无妨,但没有面向对象程序语言经验就不好办了。
关注我的微信公众号,回复“源代码4”可获得本文示例代码下载地址,谢谢各位了!
最基础的碰撞判定
为什么要加上一个“最基础的”呢,因为二维图形碰撞(Collision)一般分为Bounds接触判定(可能会碰撞)和最终碰撞判定(多边形到底是否有接触),比如下图:
首先说下Bounds是什么。我们可以认为Bounds就是一个区域,通常我们都以一个矩形作为Bounds,但有时候可能会是其他形状。就上图而言,我们的Bounds是一个矩形,并且是和XY轴平行的,这种Bounds我们成为Axis-aligned bounding boxes,简称AABB(下文提到的Bounds都是这个AABB)。一般只需要知道这个矩形的左上角和右下角两个点的位置,就能通过计算得知两个Bounds是否接触。
通常来说我们称左上角为min,右下角为max,意为该Bounds最小的点和最大的点。而我们接下来用的是[left,top],[right,bottom]分别来表示最小点和最大点的坐标。
怎么判断两个Bounds没有接触呢:
设两个不同的Bounds A和B,如果其中一个的left比另外一个的right大,或者它的top比另一个的bottom大,则我们认为 A没有和B接触。反之,判断两个Bounds是否接触的代码如下:
function overlaps(boundsA,boundsB){
if(boundsA.left > boundsB.right
|| boundsA.top > boundsB.bottom){
return false;
}
if(boundsB.left > boundsA.right
|| boundsB.top > boundsA.bottom){
return false;
}
return true;
}
简化一下上面代码:
function overlaps(boundsA, boundsB) {
return (boundsA.left <= boundsB.right && boundsA.right >= boundsB.left
&& boundsA.bottom >= boundsB.top && boundsA.top <= boundsB.bottom);
}
我就问一下,你觉得哪段代码好?我选第二个。
回到我们的游戏中,为了简化,我们认为只要树干和小鸟的Bounds接触上,就算碰撞。
那小鸟和树干的Bounds怎么得到呢。
Figure类的第1次迭代
综上,我们知道了什么是Bounds,而且注意到,要判断两个Bounds是否接触,必要条件是这两个Bounds必须在同一个坐标系下。
我们设计的Figure类具有left,top,width和height,可以用这几个属性来表示所在区域,所以我们的Figure的Bounds的最小坐标点就是[left,top],最大坐标点就是[left+width,top+height],为了便于代码阅读和简化编码,我们给Figure加上两个属性right,bottom以及一个getBounds方法:
Figure.js :
...
getBounds() {
return {
left: this.left, top: this.top,
right: this.right, bottom: this.bottom
};
}
get right() {
return this.left + this.width;
}
get bottom() {
return this.top + this.height;
}
...
如果你对Figure类第0次迭代这篇文章还有印象的话,就应该知道Figure的left和top是相对其父节点的坐标系的,所以Figure的bounds不应该这么简单地计算!比如一个Figure A的子Figure C和另一个Figure B中的子Figure D进行bounds碰撞测试,如果按照上面的代码得到的bounds来判断的话肯定是错了,坐标系都不同比较个卵。
所以我们说bounds首先是有一个相对性的,就我们的游戏而言,要判断小鸟和树桩是否碰撞,就应该获得它们相对于顶层Graph的bounds值,然后再进行判断。幸运的是小鸟和树桩(应该说是障碍地图)正好是Graph的子节点,所以上述代码正好得到它们相对于Graph的bounds,直接判断就好了。
除了相对坐标系的问题,Figure的bounds还应该要考虑到自身的旋转和拉伸,这都会影响到Bounds值,更幸运的是,我们目前这个游戏中小鸟和树桩都没有旋转和拉伸。如何计算旋转和拉伸后的Bounds我会在以后讲到变换矩阵的时候再说,先提示一下各位免得产生误会。
小鸟的Bounds还好说,就刚才代码即可获得,但是TileMap的怎么办?它的Bounds是整个地图的区域,跟树干没关系呀,这里我们就只能是特例特办了,通过TileMap的地图数据来计算地图中所有树干的Bounds。
先看下之前TileMap的drawSelf方法:
drawSelf(ctx) {
let imageManager = ImageManager.getInstance();
let trunk1 = imageManager.getImage('trunk1');
let trunk2 = imageManager.getImage('trunk2');
let trunk3 = imageManager.getImage('trunk3');
let startX = 0;
let startY = 0;
// 一列一列画:
for (let i = 0; i < this.mapData.mapData.length; i++) {
startY = 0;//每一列的y轴坐标都是从0开始
// 先计算出每一列绘制的起始x轴的值:
if (i != 0) {
// 从第二列开始就要计算了,第一列是0
startX += this.trunkWidth;// 第n列比第n-1列绝对多出一个宽度
// 再加上它们之间的间隔:
startX += this.mapData.spaceData[i-1] * this.trunkWidth;
}
// 某一列的地图数据:
let datas = this.mapData.mapData[i];
for (let j = 0; j < datas.length; j++) {
let data = datas[j];
let image = undefined;
switch (data) {
case 1:
image = trunk2;
break;
case 2:
image = trunk3;
break;
case 3:
image = trunk1;
break;
}
// 如果image为空,即地图数据为0,此处是个空白处就不需要绘制
if (image) {
ctx.drawImage(image, startX, startY, this.trunkWidth, this.trunkHeight);
}
// 画好一个就往下y坐标往下移动一个树桩高度
startY += this.trunkHeight;
}
}
}
它是通过地图数据在drawSelf中计算出树桩的位置和类型,然后绘制上去。正好,我们的树桩bounds计算跟这段代码几乎一样,那我们就可以利用这段代码来计算出树桩bounds了。
为了不重复计算,我们可以在生成地图数据的时候就计算出树桩的bounds数据,然后在绘制的时候直接使用即可,无需二次计算。所以我更改了TileMap的代码,你可以拿去和以前的比较一下:
import Figure from "./Figure";
import MapGenerator from "./MapGenerator";
import ImageManager from "./utils/ImageManager";
export default class