最近对js的小游戏开发来了兴趣,前段时间由于回答度娘知道的提问写了个贪吃蛇,虽然难度不大并不复杂,感觉还挺有意思。感觉小时候玩过的什么俄罗斯方块,坦克大战什么的都可以试着用js实现下,这天来了兴致又想写一个,其实我小时候最喜欢玩的游戏就是打砖块了,当时五年级时在学校上微机课时总是在那偷偷玩打砖块还有个雪地的保龄球还有个潜艇在深海的游戏,都忘了名字了,玩儿的不亦乐乎。可能叫法不一样,就是下图这种,想必大家都玩儿过,这里就不废话了
了解需求
大家玩打砖块都是一关一关过的,每一关(这里就打算做一关)砖块码成一个图形保持不变,飞球起于挡板弹起按直线远动,不受重力约束,遇到墙壁则按反射角方向反弹。遇到砖块则砖块消失继续反弹,同时计分,下落回来时需控制下方的挡板左右移动去接住飞球,继续弹起,砖块被打完或者飞球没接住则游戏结束。再复杂点打掉砖块时偶尔还会有道具下落,用平台接住后道具作用生效。还有那个底部的挡板有点特殊,我记得小时候玩的时候如果球打在了挡板的左方,不管球从哪个方向飞回来的都会继续反弹回左方,越偏左反弹的越厉害,右边同理。游戏的运行需求大概就是这些。
这里用js去写的话我还真没写过,但可以试试,本文算是边写代码边记录博客,如果觉得废话太多可以直接看最后的完整代码。
开发思路
首先我想到的运行原理大概是砖块都是一个个的div,飞球是会移动的div,如果飞球的div与砖块的div有重合,则砖块消失即消除掉这个html元素,砖块的坐标,飞球的坐标应该都是用div的position设为absolute,控制left,top坐标来操作的。具体飞球的移动位置应该是用向量去计算。大概就是这个思路,感觉应该不是太难。不知道是否有更好的方法去实现,也查阅过一些其它人的代码,基本都是黑色一片,绿色注释一句没有,让人有点费解。干脆先自己做一个再说。
搭建模型
首先先做个不会动的模型出来,四壁,砖块,球,挡板,计分板,首先就是四壁,画个div,这里div的width,height取值上我是这样想,既然这个游戏按坐标去运行会涉及到很多的计算。长宽最好取个整数,而不是用百分数。当然不要设的太大,最好打开网页就能全部显示没有滚动条,这样方便玩。这里我设的是600*600居中显示。代码如下:
<body style="background-color:grey;">
<div align="center">
<div style="width:600px;height:600px;background-color:#BFEFFF;border:5px groove #87CEFA;position:relative;" id="mainDiv"> </div>
</div>
<body>
运行一下如下:
然后是砖块,就是一个个div用固定位置拼上去,但得先想好个形状。记得小时候玩的时候最大的乐趣就是努力让飞球能从一个缺口上去在上面自己飞行。想来想去大盖想到这么个形状:
上面是两个对称的三角形,中间是两个竖排,下面是大个的倒三角,有点笑脸的感觉,这样可以让玩者努力将飞球从笑脸的嘴角处飞上去,如果幸运点能探入两个竖排中间来回打击。再网上飞到眼镜处就不弄那么容易了,眼镜是向下斜坡的飞球不好在上面占时间太长,不管理论能不能实现就这样写吧,依旧使用了贪吃蛇的风格,用js去自己加载这些div.
这些div都是固定的,用left top来固定坐标。为了实现对称居中效果,四壁的宽是600,那左眼处的最上方第一个div中心距离应该是四分之一处,取150.至于砖块的长宽,我取的是width:28px;height:13px;因为左右上下我都加了1px做缝隙,这样可以让每个砖块能独立出来显示分明,有人说给每个div加上border不用行了么。这里我试了有的浏览器默认border算在宽度里,即加了border宽度还是28,有的浏览器加了border则宽度变为了30.所有这里就不用border了。这样加上缝隙的话每个砖块站位刚好为整数30*15;每下一层比上一层多一个。第一层的左侧距离左壁是150 - (30/2)等于145,每下一层靠近左侧15。到第九层时靠左侧10.刚好留了个空隙。下面的排列基本都是边写变推算。不再傲述。
画完砖块再画两个div,一个挡板,一个飞球。均居中底部显示,还有一个计分板放最上面居中,坐标left,top可以自己算出代码如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title></title>
<style>
#mainDiv div{
width:28px;height:13px;background-color:blue;position:absolute;
}
</style>
<script>
var $ = function (id) {//方便按id提取
return document.getElementById(id);
};
window.onload=function(){
var x =150;var y = 15;
var m = $("mainDiv"); for(var i=1;i<=9;i++){ for(var j=0;j<i;j++){//左眼媚
var di = document.createElement("div");
di.style.top = y+(i-1)*15;//每列向下移动一行
di.style.left= x-i*15+j*30;//每列向左移动15,同行的每一个向右移动一个div宽度30
m.appendChild(di);//即i控制列的换行,j控制每行的输出这里写了个x,y其实就是从上向下画图时的起点,下面加150,加450等其实是换位置画图的意思,完全可以直接用算好的数字而不用x,y。只为清楚计算过程
//右眼媚
var di1 = document.createElement("div");
di1.style.top = y+(i-1)*15;
di1.style.left= x+300-i*15+j*30;
m.appendChild(di1);
} } for(var i=1;i<=10;i++){//左眼眼睛处不用j去控制每行,因为每行的个数位置是一样的
var di = document.createElement("div");
di.style.top = y+150+(i-1)*15;
di.style.left= x-15;
m.appendChild(di);
//右眼
var di1 = document.createElement("div");
di1.style.top = y+150+(i-1)*15;
di1.style.left= x-15+300;
m.appendChild(di1);
} for(var i=1;i<=10;i++){//大嘴
for(var j=0;j<i*2-1;j++){
var di = document.createElement("div");
di.style.top = y+450-(i-1)*15;
di.style.left= x+165-i*30+j*30;
m.appendChild(di);
}
}
}
</script>
</head>
<body style="background-color:grey;">
<div align="center">
<div style="width:600px;height:600px;background-color:#BFEFFF;border:5px groove #87CEFA;position:relative;" id="mainDiv">
<div id="markDiv" style="width:60px;height:25px;top:0;left:270;border:1px dotted blue;background-color:#BFEFFF"></div>
<div id="qiuDiv" style="width:10px;height:10px;top:580;left:295;background-color:red;"></div>
<div id="bangDiv" style="width:150px;height:10px;bottom:1;left:225;background-color:black;"></div>
</div>
</div>
<body>
运行结果如下
火狐,IE,谷歌,360,显示结果基本一样。如果大家有更好的图形画法可以自己设计。
加入控制
游戏开始前是可以先找个好位置出击的,即飞球飞起前可以随挡板左右移动,就先做个左右的移动。这里就用在body上加键盘监听事件了。用左右箭头控制好了。同时还得有一个boolean值来确定当前游戏是否已经开始,如果已经开始了飞球就不要随挡板移动了,代码如下:
var bangleft = 225;//取挡板当前的左坐标,就是left距离
var qiuleft = 295;//取飞球当前的左坐标
var bs = 10;//按一次键移动多少,
var kflag = false;//标记游戏是否已经开始
//键盘处理事件
function keydownEvent(event){
var bang = $("bangDiv");//取挡板div
var qiu = $("qiuDiv");//取飞球的div
if(event.keyCode==37){//如果是左箭头
for(var i = 0;i<bs;i++){
if(bangleft-1!=0){//0即left为0,即已经到左移动到了墙壁,就不再起作用。
bangleft-=1;//每一次向左移动1,其实上面做了for循环,结果就是每按一次向左移动了bs=10,为什么要循环着去加而不一次性去加,原因很简单就是为了防止一次就加过了超出了范围,同时我们可以通过设定bs参数的值来改变挡板移动的快慢
bang.style.left = bangleft+"px";//改变挡板位置
if(!kflag){//如果游戏没开始
qiuleft-=1;
qiu.style.left = qiuleft+"px";//改变飞球位置
}
}
}
}
if(event.keyCode==39){//如果是右箭头同理
for(var i = 0;i<bs;i++){
if(bangleft+1!=450){//检测是否碰右壁
bangleft+=1;
bang.style.left = bangleft+"px";
if(!kflag){
qiuleft+=1;
qiu.style.left = qiuleft+"px";
}
}
}
} }
body要加上<body style="background-color:grey;" onkeydown="keydownEvent(event)">
飞球的运行
然后先不考虑打砖块,先让飞球可以飞起来,无视砖块*在四壁内运动,这里还有一点我们应该都知道,小时候玩的时候飞球飞起时并不是直线飞起的,总是偏左或偏右那么一点,因为如果设定直线向上的话挡板也保持不动,游戏开始后飞球就回一直来回上下做运动。
先不管角度问题,先想怎么移动,这里要复习一下中学的课程了看下图:
假设球在x轴y轴的(0,0)为起点,以α角移动,假设每个单位时间内移动了s(速度).就是图中的斜线距离。到了坐标(x1,y1).那已知s的值和α的值就可求出移动的距离x1,y1。赋值给球的坐标做为新坐标,球就移动了,这样我们用setTimeout函数不断去调用这个方法球就可以以直线自己前进了。也先不考虑撞不撞强,假设α=30度,s=1;那单位时间内飞球的新坐标就是(s/sin30,s/cos30)如果忘了正弦余弦的用法可以去百度,我确实忘了。。所以飞球按30度角直线运行的方法就是如下代码:
var qx = 295;//飞球初始坐标left
var qy = 580;//飞球初始坐标top
function go(){
var qiu = $("qiuDiv");
qx = qx +1*Math.cos((2*Math.PI/360)*30);
qy = qy -1*Math.sin((2*Math.PI/360)*30);
qiu.style.left = qx+"px";
qiu.style.top = qy+"px"; setTimeout("go()",10)
}
这里要说明js里cos,sin的用法,Math.sin(x) 表示的是x 的正弦值。Math.cos(x) 是 x 的余弦值。但js里的这个x是弧度而不是角度,假设角度是α,转化为弧度就是
2*Math.PI/360*α我们这里假设的是α=30度。直角三角形里知道一个锐角α和斜边长s=1,求α的对边y1的长就是s*sin(α)在此例中即为
1*Math.sin((2*Math.PI/360)*30);
求a的邻边x1就是cos....同理
由于我们现在假想的是左上飞,所以是x取加值,y取减。然后循环调用go(),10是间隔时间,即0.01秒。s=1相当于限制了每次移动的位移,如果s设的过大,则球会变快而且平滑效果很差几乎是瞬移,所以是s设的小,然后让间隔时间变短,这样平滑效果好点,大家可以自己更改调试。现在大概飞行的原理知道。然后就考虑撞墙了。
一开始我钻了牛角尖(此段为本人的一段傻瓜笨蛋推理,容易严重误导人,不愿看的可以直接看下面的红字‘最终其实原理是这样的’),总是考虑要是碰的是左壁,或者右壁,去区分这些,然后去改变角度什么的,后来一些如果详细区分的话后面还要考虑砖块,那就要去考虑碰的是砖块的上下左右,太麻烦了,就好像那个故事有个皇帝要在每条街铺上地毯防止脚踩伤,而大臣建议何不让脚去穿个鞋呢,忘了故事大概了就是这样。我们应该考虑的是这个飞球是上下左右哪个边碰壁了。这样需要考虑的就少了。
如何判断碰壁呢,这里不考虑砖块的情况下很简单。就是去计算坐标罢了。那角度是怎么改变的呢,原理那提到是反射角,飞球的右边如下图:
后来我又继续画图上边碰壁右边碰壁画完我发现
最终其实原理是这样的其实折腾来折腾去这里都是四方形,α的值是不会变的,角度的改变都是在围绕α在做文章。同样单位时间内的移动距离s也是不会变的,x方向y方向的移动距离的绝对值也是不会边的,其实撞墙就是改变x,或y位移的正负而已,至于角度我们没必要去考虑。添加代码如下:
var qx = 295;//飞球初始坐标left
var qy = 580;//飞球初始坐标top
var jiao = 89;//初始飞行角度
var zx = 1;//控制left位移的正负
var zy = -1;//控制top位移的正负
var rp = null;//控制游戏进程
function go(){
var qiu = $("qiuDiv"); qx = qx +zx*Math.cos((2*Math.PI/360)*30);
qy = qy +zy*Math.sin((2*Math.PI/360)*30);
qiu.style.left = qx+"px";
qiu.style.top = qy+"px";
if(qy>=580){
//alert(qx)
//alert(qx<bangleft||qx>bangleft+150)
if(qx<bangleft||qx>bangleft+150){//判断是否接住
clearTimeout(rp);
}else{
zy=-1;
rp = setTimeout("go()",1);
}
}else{
if(qx>=600)zx=-1;
if(qx<=0)zx=1;
if(qy<=0)zy=1;
rp = setTimeout("go()",1);
}
}
判断与砖块碰撞
这样基本实现了接球并移动。但不管怎么接球都是按原来的角度走的,运行起来球是一直走一条轨道。这里就用到开始需求那里说的。
如果球打在了挡板的左方,不管球从哪个方向飞回来的都会继续反弹回左方,越偏左反弹的越厉害,右边同理。而且碰到砖块虽然不会改变虽然α没变都轨迹会变。这里要解决两个问题了。一个是改变角度,一个是碰撞砖块。貌似后一个更难点,但刚写完撞墙顺思路可以继续想如何判断撞砖块。
我又一次想到了数组。就是生成砖块的同时把每个砖块放入数组中去,然后飞球每移动就与所有砖块进行比较是否碰撞。则开始的加载代码改为:
var zdivs = new Array();//用于存储所有的砖块。 window.onload=function(){
var x =150;var y = 15;
var m = $("mainDiv"); for(var i=1;i<=9;i++){ for(var j=0;j<i;j++){
var di = document.createElement("div");
di.style.top = y+(i-1)*15;
di.style.left= x-i*15+j*30;
m.appendChild(di); var di1 = document.createElement("div");
di1.style.top = y+(i-1)*15;
di1.style.left= x+300-i*15+j*30;
m.appendChild(di1); zdivs[zdivs.length]=di;
zdivs[zdivs.length]=di1;
} } for(var i=1;i<=10;i++){
var di = document.createElement("div");
di.style.top = y+150+(i-1)*15;
di.style.left= x-15;
m.appendChild(di); var di1 = document.createElement("div");
di1.style.top = y+150+(i-1)*15;
di1.style.left= x-15+300;
m.appendChild(di1); zdivs[zdivs.length]=di;
zdivs[zdivs.length]=di1;
} for(var i=1;i<=9;i++){
for(var j=0;j<i*2;j++){
var di = document.createElement("div");
di.style.top = y+450-(i-1)*15;
di.style.left= x+150-i*30+j*30;
m.appendChild(di); zdivs[zdivs.length]=di;
}
} }
然后在飞球移动的js后面加上循环判断,如果相碰则将此砖块在数组中移除同时设置display为none;然后难点就是如何判断飞球与某一个div相碰,相碰又是以何种方式反弹。想想就这几种情况,飞球的右边与砖块左边碰,或者下边与砖块上边碰等等
判断两个div是否相碰,即有重合点的方法其实是数学上判断两个矩形是否重叠。
假设两个矩形的位置如图,两个矩形a和b,分别有4个边,ax1是左边的x坐标,ay1是上班的y坐标,ax2是右边的x坐标,。。。。。我们取a,b的同侧边最大的那一边组成的新坐标sx,sy,如果这个坐标同时存在与两个矩形内就说明两个矩形重合。即ax1与bx1比较取最大的bx1,ay1与by1取最大的by1。因为这里是用的top,left所以越靠左left越大,越靠下top越大。这个的具体道理就不好说了,好好看几何吧。
相碰的问题解决了,那飞球是从哪侧碰撞的呢?该反弹回哪边?这里我的思路是飞球a,砖块b,可能的撞击方式是a左碰b右,a右碰b左,a上碰b下,a下碰b上四种,这里暂不考虑角碰角原路返回。按四种情况的话a左碰b右的话,a左与b右的left差值,与后面三个a右b左的left差值,a上b下的top差值,a下b上的top差值,这四个差值的绝对值应该是哪个最小判定为是哪种情况相撞。个人是这么理解,如有异议欢迎提出。所以知道了是那种情况的碰撞,也就明白了该如何去改变方向即zx,zy的值。
然后有了思路则飞球每走一步就去与数组里的div遍历去对比是否相碰。继续写在上面top<=580后面
function go(){
var qiu = $("qiuDiv"); qx = qx +zx*Math.cos((2*Math.PI/360)*jiao);
qy = qy +zy*Math.sin((2*Math.PI/360)*jiao); if(qy>=580){
if(qx<bangleft||qx>bangleft+150){//判断是否接住
clearTimeout(rp);
}else{
zy=-1;
rp = setTimeout("go()",1);
}
}else{
for(var i=0;i<zdivs.length;i++){
var io = checkIsP(qx,qy,zdivs[i].offsetLeft,zdivs[i].offsetTop);
if(io!=0){
zdivs[i].style.display = "none";
zdivs.splice(i,1);
if(io==1){
zx=1;
}
if(io==2){
zx=-1;
}
if(io==3){
zy=1;
}
if(io==4){
zy=-1;
}
break;
}
} if(qx>=600)zx=-1;
if(qx<=0)zx=1;
if(qy<=0)zy=1;
qiu.style.left = qx+"px";
qiu.style.top = qy+"px";
rp = setTimeout("go()",1);
}
}
function checkIsP(qx,qy,zx,zy){
var f = {
x:qx,
y:qy,
x1:qx+10,
y1:qy+10
}
var z = {
x:zx,
y:zy,
x1:zx+30,
y1:zy+15
}
var sx;var sy;
sx = f.x>=z.x?f.x:z.x;
sy = f.y>=z.y?f.y:z.y;
if(sx >= f.x && sx <= f.x1 && sy >= f.y && sy <= f.y1 && sx >= z.x && sx <= z.x1 && sy >= z.y && sy <= z.y1){ return seSmall(Math.abs(f.x-z.x1),Math.abs(f.x1-z.x),Math.abs(f.y-z.y1),Math.abs(f.y1-z.y)); }else{
return 0;
}
} function seSmall(a,b,c,d){ if(a<b&&a<c&&a<d){
return 1;
}
if(b<a&&b<c&&b<d){
return 2;
}
if(c<a&&c<b&&c<d){
return 3;
}
if(d<b&&d<c&&d<a){
return 4;
}
}
这样碰撞问题基本解决了
更改角度
开篇还提到要按挡板的接球位置来更改反弹的角度。其实这个想想就简单,就按接触挡板时的距离对角度做百分比就行了与挡板接触处代码改为如下
if(qy>=580){
if(qx<bangleft||qx>bangleft+150){//判断是否接住
clearTimeout(rp);
}else{
zy=-1;
if((qx-bangleft)>(75)){
jiao = 90-(qx-bangleft+10-75)/75*90;
zx = 1;
}else{
jiao = 90 - (75-(qx-bangleft+10))/75*90;
zx=-1;
}
rp = setTimeout("go()",1);
}
基本这个游戏就完成了,感觉还是设计的挺烂的,每次移动都耗那么多计算,其实在挡板与最下层砖块间的空白可以省去不用计算,因为这一空间根本没砖块,所有在计算for循环前加上
else{if(qy<=480)
for(var i=0;i<zdivs.length;i++){
还有一点也可以优化下,就是砖块都是一行一行排列的。所有可以对砖块进行按行分组。当飞球飞到此行的高度时只对比此行的砖块,还有积分模块也不是很难,这里就不再写了,太长了。源代码见上文,算是抛砖引玉。