代码
DEMO
不管写的过程中觉得有多便秘,写完了回过头再去看这个游戏其实并不算多么的复杂,一些基本的问题处理好就行——这也是这篇文章所想要说明的东西。因此这篇博客只能算是记录了一下写一个游戏过程中的一些思路,如果有同学也想要自己写一个游戏并不知道如何开始的话,我推荐下面两个内容:
如何开发一个简单的HTML5 Canvas 小游戏
HTML5小游戏—爱心鱼
如果需要玩家通过按键操控坦克进行运动,很多人第一个想到的应该就是把相应的运动函数绑定到相应按键的onkeydown事件之上。
一般来说这么写有一个问题,那就是为了防止诸如像老人松手慢导至键盘事件多次触发这种情况,只有当你按下按键到一定的时间以后事件才会连续进行触发。
这个问题反应到游戏上就是你的坦克总是要在你按下按键后过一段时间才会开始连续运动,非常影响游戏体验。
这个问题的解决方法很简单:
let keyInfo = {}; //按键是否被按下的信息
let aKey = [72 , 74 , 87 , 83 , 65 , 68 , 38 , 40 , 37 , 39 , 17]; //这里面的数字是wasdhj等按键的键值
for (let i = 0; i < aKey.length; i++) {
keyInfo[aKey[i]] = {
pressed : false
}
}
在不提坦克与子弹之间的碰撞问题的前提下,路径问题基本上就是在确定你的坦克跟子弹(子弹的问题其实更复杂一点,后面再详细讨论)在地图上哪里能走哪里不能走,虽然这个问题并不是很复杂,但在我看来这个问题可以说是整个游戏的核心所在,因为后面很多问题都是围绕着路劲而来。
要搞清楚路劲问题还是先要有一些准备工作:
游戏的主界面大小为416*416像素,总共由13*13个32*32像素的区域构成。
游戏中的坦克,障碍物及奖励的图片的大小都是32*32像素,因此只要使用一个13*13的数组就能将整个地图数据给存储下来了。
障碍物图片:
let mapData[0] =[
//0代表空白,1代表32*32的砖块,2代表32*16的砖块,后面类推
[0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0],
[0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0],
[0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0],
[0 , 1 , 0 , 1 , 0 , 1 , 6 , 1 , 0 , 1 , 0 , 1 , 0],
[0 , 1 , 0 , 1 , 0 , 2 , 0 , 2 , 0 , 1 , 0 , 1 , 0],
[0 , 2 , 0 , 2 , 0 , 4 , 0 , 4 , 0 , 2 , 0 , 2 , 0],
[4 , 0 , 4 , 4 , 0 , 2 , 0 , 2 , 0 , 4 , 4 , 0 , 4],
[7 , 0 , 2 , 2 , 0 , 4 , 0 , 4 , 0 , 2 , 2 , 0 , 7],
[0 , 4 , 0 , 4 , 0 , 1 , 1 , 1 , 0 , 4 , 0 , 4 , 0],
[0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0],
[0 , 1 , 0 , 1 , 0 , 2 , 0 , 2 , 0 , 1 , 0 , 1 , 0],
[0 , 1 , 0 , 1 , 0 , 18 , 4 , 17 , 0 , 1 , 0 , 1 , 0],
[0 , 0 , 0 , 0 , 0 , 3 , 15 , 5 , 0 , 0 , 0 , 0 , 0]
];
有了地图数据以后,只要循环调用drawImage方法就能够将地图画出来了:
for (let i = 0; i < 13; i++) {
for(let j = 0; j < 13; j++){
//获取对应的值
let iData = mapData[0][i][j];
if (iData) {
//如果获取的值不为0,那么开始绘制地图
cxt.drawImage(oImg, 32 * iData, 0, 32, 32, 32*j, 32*i, 32, 32);
}
}
}
第一关界面:
其他先不说,看这张图,最大的那个32px的方块表示的就是一个坦克,左上角顶点为其坐标,四个8px的小方块表示四个方向发射的子弹,坐标也是左上角。
很明显当坦克在发射子弹的时候,子弹的初始坐标必须要根据坦克的坐标及方向进行调整,不然发射的子弹位置就不对了。
//iDir表示坦克的方向,x和y的初始值也是坦克的坐标,这里需要根据坦克的方向进行调整才是子弹的初始坐标
// 1、3
if (iDir%2) {
y += 12;
x += 24*(+!(iDir-1));
// 0, 2
} else {
x += 12;
y += 24*iDir/2;
}
大家可以看上面那个第一关的图片,很容易就能看出来没有砖块的黑色路径其实跟坦克的宽度是差不多的,当然,他们的宽度都是32个像素。
那么问题来了,游戏中玩家的坦克每次循环(一次循环16毫秒到17毫秒不等)会移动两个像素,除了一开始坦克正好对准了位置以外,以后每次转换方向,玩家根本没办法做到每次都是分毫不差的卡到那个32像素的点上,那么按照游戏的一般规定,对不起你前面有障碍物你无法通过。。。
因此,我们需要在坦克每次改变方向之后,都要正好对准这么一个点,代码如下:
// 在坦克转换方向后重新定位坦克的位置,使坦克当前移动方向的左边正好能够整除16,这样就正好对齐了砖块的契合处
//iDir表示坦克当前的方向02表上下,13表右左
//x表示坦克当前的横坐标,y表示坦克当前的纵坐标
if (iDir % 2) {
y = Math.round(y / 16);
} else {
x = Math.round(x / 16) * 16;
}
如果仔细看了代码,可能有人心中就会出现一个疑问,为什么是要能够整除16?OK,下面就来回答这个问题。
先回过头来看看上面那张障碍物的图片,拿灰褐色的砖块来说,很明显可以看到砖块一共有四种尺寸7张图,最小是16*16px,最大是32*32px。
想要告诉坦克或者子弹哪里有障碍物能否通过有两种方式:
一是将每一种状态的砖块都保存下来,这样砖块跟钢筋加起来共十四种状态,判断起来过于麻烦,而且子弹打掉砖块后的判断也相应增加了变化的情况。
二是将砖块都分解为16*16的小砖块,这样就不需要判断砖块的尺寸了,然后用一个26*26的数组就能够将整个地图的路径情况给记录下来。
令:其实这里还有一个方法那就是把障碍物全部分解为16*16的尺寸,这样地图数据直接就是路径数据了。
坦克的碰撞,这里面又包括了:
子弹的碰撞,这里面又包括了:
①、坦克与奖励以及子弹与子弹的碰撞的检测代码基本上没啥区别,因此只举坦克与奖励的碰撞来说明:
//坦克与奖励的碰撞检测
//坦克的x、y坐标分别减去奖励的x、y坐标,如果都小于一个坦克的大小32,那么表明坦克与奖励已经碰撞
let xVal = Math.abs(tank.x - bonus.x),
yVal = Math.abs(tank.y - bonus.y);
if (xVal < 32 && yVal < 32) {
//碰撞了,执行相应的代码
}
如上面代码所示,他们之间的碰撞检测主要就是检查横纵坐标之差的绝对值,如果这两个值都小于坦克本身的尺寸,那么表明他们碰到了一起。
子弹同理,不过是检测是否小于子弹本身的尺寸8就可以了。
②、表面上看坦克与坦克的碰撞检测似乎与坦克与奖励、子弹与子弹的碰撞没什么不同,实际上还是有区别的,下面用一张图说明:
那么坦克之间的碰撞检测如下:
//同样是检查x、y坐标的差值的绝对值
let xVal = Math.abs(this.x - tank.x),
let yVal = Math.abs(this.y - tank.y);
//这里根据方向的不同,检测的值也不同,这里的26留下的余地,如果两个坦克正好重叠,那么他们也是可以运动的
//iDir表示坦克当前的方向02表上下,13表右左
if (iDir % 2) {
//iDir的值为1或者3,也就是坦克的方向是左右
if (xVal < 32 && xVal > 26 && yVal < 32) {
//...
}
} else {
//iDir的值为0或者2,也就是坦克的方向是上下
if (yVal < 32 && yVal > 26 && xVal < 32) {
//....
}
}
这里判断的值之所以为26,拿y坐标来举例,如之前坦克转向后对齐里面的y = Math.round(y / 16)所示,在坦克转向后坦克坐标是会四舍五入的,因为移动速度最慢的坦克每个循环会移动1px,因此当(y / 16)< n.5的时候,n*16+1px ~ n* 16+7px会被舍弃,最多是6px,这样检测值是32-6=26正好能够让两个坦克在重叠后通过转向可以继续运动。
当然这里也会导至一个BUG,那就是某个时候如果我的坦克正好转向,坐标四舍五入后,有可能会导至两个坦克重叠,所以这里也需要在坦克转换方向后的做一个碰撞检测,如果正好有重叠那就不往那个方向转。
子弹与坦克的碰撞又是另外一回事了,之前也讲过子弹的坐标是根据发射子弹的坦克的坐标重新定位过的,因此检测的判断条件跟子弹的方向有很大的关系:
let x = bullet.x - oTank.x;
let y = bullet.y - oTank.y;
if (this.iDir % 2) {
return (this.iDir -1)
? (x < 32 && x > 0 && y > -8 && y < 32)
: (x > -8 && x < 0 && y > -8 && y < 32);
} else {
return this.iDir
? (y > -8 && y < 0 && x > -8 && x < 32)
: (y < 32 && y > 0 && x > -8 && x < 32);
}
用方向向上的子弹来举例:
当坦克的x坐标位于横着的绿色线条中间之时(-8 <= bullet.x - tank.x <= 32),就表示子弹与坦克在横坐标上相碰撞了。
当坦克的y坐标位于竖着的绿线区域内时(0 <= bullet.y - tank.y<= 32),表示子弹与坦克在纵坐标上相碰撞了。
两个条件何在一起,就是方向向上的子弹与坦克的碰撞条件:
y < 32 && y > 0 && x > -8 && x < 32
坦克与障碍物的碰撞实际上就是去判断最早的那个26*26的路径数组,看坦克当前方向上所对应的两个数组所代表的障碍物是否允许坦克通过。
代码并没有什么难度,唯一需要注意的是当坦克的方向是向上跟向左的时候,需要分别将传入的y与x坐标-1,这是因为你需要判断的是下一个路径数组的值,而不是当前。
如果说子弹一次能打掉最少打掉的是16*16大小的障碍物的话,想要处理也非常简单,根据坐标将对应区域给cxt.clearReact掉,再将相应的路径数组置0就能够解决。
可惜问题并不是这么简单,为什么复杂呢?看下图就明白了:
let oBrickStatus = {}; //建立一个对象用来保存被子弹击中的砖块的状态
let iIndex = x/16*26 + y/16; //因为路径数组的key值是使用x/16与y/16计算而来了,那么我们将这两个key值处理一下后得到一个新的数值,这个值用来作为记录被子弹击中的砖块的状态的key值
//如果oBrickStatus中没有保存这个砖块对应的记录,那么将[1, 1, 1, 1]赋值给oBrickStatus[iIndex]
//[1, 1, 1, 1]是因为一个16*16的区域正好可以分成4个8*8的区域,因此用这个数组记录下当前16*16的区域有哪些8*8的区域不存在(不存在的数组值为0)
if (!!oBrickStatus[iIndex]) {
//这个函数用来计算子弹击中砖块后如何进行处理,下面单独进行介绍
hitBrick();
} else{
oBrickStatus[iIndex] = [1, 1, 1, 1];
hitBrick();
}
经过上面那段代码,我们只需要去计算oBrickStatus[iIndex]的值,就将这个砖块的状态给保存了下来,如果以后子弹再打中了这个砖块,那么就拿出oBrickStatus[iIndex]的值来检查就可以了。
我们拿子弹方向向上来举例:
子弹向上的时候首先会检查索引值为2和3的数组项,当这两个值中间有一个不为0的时候,表明子弹与砖块碰撞了,那么使用clearReact清空掉相应的区域,并将索引值为2和3的数组项置0。
如果两个值都为0,那么子弹继续运动,再运动了8个像素后进入了数组项0和1表示的区域,此时再检查这两块区域所代表的是否为0,重复之前的操作。
最后在确定一个16*16的砖块全部被打掉后,直接将路径数组中的数据由表示砖块的1置为0,这样就实现了子弹对砖块的击中后的效果了。
以上就是我对于这个游戏的一些思考了,这些问题解决后整个游戏感觉就没什么需要注意的地方了,剩下的就是写了~~~