本着好记性不如烂博客以及分享成功的喜悦和分享失败的苦楚,今天我来分享下一个练手项目:打飞机游戏~从小就自己想做游戏,可是一直没有机会。HTML5给了我们这个平台,这个平台可以有很多以前想都不敢想的东西存在,让我们怀着感激与敬畏的心情,开始我们HTML5的旅程。
练手:打飞机游戏---思路。
1 需求:
这个不多说了,玩儿过雷电的同学都知道,有己机,敌机,还有子弹,当然BOSS也算但是BOSS也是敌机了。
2 分析:
2.1 开发工具: 采用HTML5+JS来完成,JS面向对象虽然有点复杂,但是很好用。采用Function作为对象的载体,返回的是Object(后面会详细讲到,这里只是先说说开发工具),采用HTML5的canvas对象作为表现层。
2.2 游戏原理:大家都喜欢打灰机吧,可是谁知道打灰机是怎么实现的呢?且容我抽丝剥茧,宽衣解带,沐浴更衣,慢慢道来~ 大体的思路是,图片一帧一帧地更新自己在canvas上的位置,利用人眼的缓存实现动画的效果。在更新图片之前更新坐标,在更新坐标之前更新状态。
2.3 游戏架构: 采用mvc模式,这种模式应该大家都不陌生,Model层就是在屏幕上显示的元素它的实体类,实体类有自己的方法包括更新自己的坐标,发生碰撞时的反应等,它也有自己的私有变量,比如坐标,图片,爆炸效果图片,碰撞体积,等等。view层就是显示层,它只根据返回的图片更新到画布上与业务无关。Service层,Control层与Model层之间的一层,它拥有所有要在屏幕上显示的实体(除了背景)的引用,处理Model层返回的事件,并缓存View层上要显示的内容,经过处理后返回要更新的图片信息给Control层。Control层,它会处理游戏中的事件,连接view层,并且会更新游戏的进度。它调用service,得到返回结果更新view层。
思路介绍完了,下面将介绍打灰机的具体实现了。
3.Model层
从己机,敌机,子弹中提取它们的共同点:它们都可以飞,,,,因此我把它们的父类叫fly,然后竭尽脑汁去想它们其他的共同点:它们都可以爆炸,它们会发生碰撞,它们都会移动,它们都可能会超出边界。目前我想到的就这么多了。但是它们的私有变量却都是大同小异的。它们都有一个hp属性代表血值,有个x代表x坐标,有个y代表y坐标,有个图片的引用表示要在屏幕上显示的图片,有个图片的引用表示要显示爆炸的图片,有个目标表示自己的愿景:是要消灭向上飞的呢还是要消灭向下飞的还是除了自己要消灭一切,不管怎么说得有个愿景。因此,把这个类抽象出来作为所有要表现在屏幕上的飞行物的父类:fly。我们看下它的继承关系(由于没太多接触过类图,画的粗糙,大家对付着过吧,聊胜于无。)右边的是属性和方法,属性是一个javascript中的Object
这里我们先给出fly和bullet的代码,其他的就自己扩展
fly.js:
1 /*飞行物类,不管敌机,boss,己机,还是子弹,都是飞行物,它们都有改变位置,碰撞,越界判定的方法,也都有碰撞体积,图像,坐标,序号和hp等属性。具体参数如下: 2 *var spec={ 3 x:1,画布中的横坐标 4 y:CANVAS_HEIGHT-5,//画布中的纵坐标 5 hp:1,//飞行物的血量 6 index:svc.total(),//序列号 7 exploreImg:getImg("img/blasts3.png"),//爆炸的图片 8 img:getImg("img/mybullet2.png"),//自身图片 9 target:0,//目标 10 conflictSquare:20,//碰撞体积 11 speedX:5,//X方向的速度 12 speedY:2,//Y方向的速度 13 movex:0,//X轴移动的方向 14 movey:0//Y轴移动的方向 15 }; 16 *这个对象完全可以用json来代替!可扩展性就不多说了。 17 *它大多数方法(除了对私有变量访问的方法)均返回数组格式为:[{func:'',params:[]},{func:'',params:[]}]看到了什么?很像json吧?不过不是用json来调用的, 18 *它是在控制层(service)接收,然后把有用的信息注册到一个array中来管理,根据func这个字段的不同,调用不同的方法。对象有index,删除的时候就可以很方便的删掉了。 19 *整体来说,这个fly类是所有用到的模型的父类, 20 *这种以方法(function)来构建对象的形式是安全地,因为它返回的是有很多属性的对象(return{a:function(){},b:function(){},,,,};),对象的所有内容均可访问其私有变量, 21 *但是脚本不能在外部访问其自身私有变量。这对于透明性和扩展性都是有好处的。 22 */ 23 var fly=function(spec){ 24 25 var imgWidth=spec.img.width;//图片的宽度,该变量是私有变量,当fly这个函数完蛋了,它的私有变量仍然可以访问,神奇吧,这就是javascript的魅力。 26 var imgHeight=spec.img.height;//图片的高度 27 return { 28 move:function(directionX,directionY){//根据方向来移动0,1表示Y轴向下。1,0表示X轴向右,依次类推 29 30 31 spec.x=(directionX||0)*spec.speedX+spec.x;//水平移动,任何移动的类型,只要不要太变态,应该都能调用这个方法来改变坐标,如果有不能调用的再说。 32 33 spec.y=(directionY||0)*spec.speedY+spec.y;//垂直移动,其他同上。 34 35 }, 36 hpChange:function(newHp){//碰撞了,血量改变了或者飞机挂掉了就要用到这个方法改变私有变量的值 37 spec.hp=newHp; 38 }, 39 x:function(){//得到画布中的x坐标 40 return spec.x; 41 }, 42 y:function(){//得到画布中的y坐标 43 return spec.y; 44 }, 45 hp:function(){//血量,访问私有属性血量要用到这个方法 46 return spec.hp; 47 }, 48 index:function(){//这个序列属性是用来删除自己的,使用array的splice方法,然后我们再做些手脚,就可以让index和array的序列相同啦。 49 return spec.index; 50 }, 51 setIndex:function(ndex){//这个方法用来改变自身的序列 52 spec.index=ndex; 53 }, 54 exploreImg:function(){//得到爆炸的图片,如果爆炸事件发生,图片被缓存,之后与其他内容一起画到画布上去。 55 return spec.exploreImg; 56 }, 57 img:function(){//得到自身图片,之后在控制层(service)缓存 58 return spec.img; 59 }, 60 target:function(){//得到敌对目标的编号,敌飞行物和友飞行物当然不能一样了。 61 return spec.target; 62 }, 63 function(){//图片的宽度 64 return spec.img.width; 65 }, 66 height:function(){//图片的高度 67 return spec.img.height; 68 }, 69 cflctSqr:function(){//碰撞体积(是一个正方形,这个方法得到碰撞体积的边长) 70 if(spec.conflictSquare){ 71 return spec.conflictSquare; 72 } 73 return 0; 74 }, 75 onConflict:function(other){//事件,后台判断碰撞,当碰撞发生时就调用这个方法,返回一个事件交给控制层处理。 76 //alert(spec.hp+" "+other.hp()); 77 if((spec.hp>0) && (other.hp()>0)){//如果碰撞接收者和碰撞发出者的hp属性都大于零,才能进行碰撞,碰撞的结果就是它们的血值相减 78 //这又造成了有一个血值变为了零或负,或者两者同归于尽 79 //dispear 这个事件代表消失,即如果一个飞行物被干掉了,我就让后台来处理这个被干掉的家伙的后事,把它从生龙活虎的队列中删除~ 80 //disapear函数可以接受多个参数,这个参数是一个数组,数组包含多个消失的对象(当然一般都是只会消失一个对象)。 81 82 var hpo=other.hp();//hpo就是传入的参数的hp,两个飞行物碰撞,就假设一个是主动者,这样好分析点。 83 var hps=spec.hp;//hps是被碰的hp 84 spec.hp -= hpo;//被碰的hp减去碰撞的hp 85 other.hpChange(other.hp()-hps);//碰撞的hp再减去被碰的hp这两件事造成了如下的代码来判断碰撞双方的状态 86 var returnArray=[];//返回一个事件的集合先给他初始化为一个数组 87 //alert(spec.hp+" "+other.hp()); 88 if(spec.hp == 0 && other.hp() == 0){//如果同归于尽,两者都爆炸,两者都消失 89 returnArray=[]; 90 returnArray.push({func:"explore",params:[other.exploreImg(),other.x(),other.y()]});//碰撞发起的爆炸事件 91 returnArray.push({func:"explore",params:[spec.exploreImg,spec.x,spec.y]});//碰撞接受者的爆炸事件 92 returnArray.push({func:"disapear",params:[spec.index]});//碰撞接收者的消失事件入栈 93 returnArray.push({func:"disapear",params:[other.index()]});//碰撞发起者的消失事件入栈 94 95 96 return returnArray; 97 98 99 }else if((spec.hp <=0) && (other.hp()>0)){//如果碰撞发起者它的hp远比接受者要大,那么接收者爆炸,消失 100 returnArray=[]; 101 returnArray.push({func:"explore",params:[spec.exploreImg,spec.x,spec.y]}); 102 returnArray.push({func:"disapear",params:[spec.index]}); 103 return returnArray; 104 105 }else if((spec.hp > 0) && (other.hp()<=0)){//如果碰撞发起者它的hp小于接受者,那么碰撞发起者爆炸,消失。 106 returnArray=[]; 107 returnArray.push({func:"explore",params:[other.exploreImg(),other.x(),other.y()]}); 108 returnArray.push({func:"disapear",params:[other.index()]}); 109 return returnArray; 110 }else{//这种情况我不知道为什么可以发生,因此不打算处理这种情况,但为了保险起见返回一个undefined。 111 return ; 112 } 113 } 114 115 }, 116 judgeBundle:function(){//超出边界把它揪回来。 117 118 if(spec.x<=0){//如果横坐标超出了左边的界限,揪回来。 119 spec.x=0; 120 }else if(spec.x>=(CANVAS_WIDTH-imgWidth)){//如果横坐标大于画布边界与图片边界的差就是到了边缘 121 122 spec.x=CANVAS_WIDTH-imgWidth;//让它等于边界的值 123 124 } 125 if(spec.y<=0){//和x轴类同。 126 spec.y=0; 127 }else if(spec.y>=CANVAS_HEIGHT-imgHeight){ 128 spec.y=CANVAS_HEIGHT-imgHeight; 129 } 130 }, 131 onMove:function(){//onMove:移动方法->judgeBundle->move,此处只是循环的移动。,当然也可以自己定义移动方法,但要在子类中定义了。 132 133 if(spec.y>CANVAS_HEIGHT-imgHeight||spec.y<0){//如果到了最上边,或者最下边就向相反方向移动 134 spec.movey=-spec.movey; 135 136 } 137 if(spec.x>(CANVAS_WIDTH-imgWidth)||spec.x<0){//类似于上面的判断 138 spec.movex=-spec.movex; 139 140 } 141 this.judgeBundle();//判断是否超出边界 142 this.move(spec.movex,spec.movey);//移动 143 144 return {type:"drawimg",func:"drawimg",params:[spec.img,spec.x,spec.y]};//移动了,把移动事件返回去,缓存图片和坐标,最后一起画出来。 145 } 146 }; 147 };
bullets.js:
1 /*子弹类,继承了fly类,增加了Function类的method方法,给Object增加了superior方法使其可以调用父类的方法。,增加了isbullet方法。 2 *改写了onConflict方法使子弹之间不能发生碰撞。 3 */ 4 var bullets=function(spec){ 5 var isbullet=true;//是否子弹(父类没有) 6 var that=fly(spec);//子弹类继承fly类 7 Function.prototype.method = function(name,func){//为Function增加method,调用模式为:function1.method('xxx',function(){}); 8 if(!this.prototype[name]) { 9 this.prototype[name] = func; 10 } 11 return this; 12 } 13 Object.method('superior',function(name){//调用上面的method方法,给Object增加父类的内容。 14 var that=this; 15 var method = that[name]; 16 return function(){ 17 return method.apply(that,arguments);//第一个参数为上下文,第二个参数为传递的参数。 18 }; 19 }); 20 var super_onConflict=that.superior('onConflict');//父类的onConflict方法 21 that.onConflict=function(other){ 22 if(typeof other.isbullet === 'function' && other.isbullet()){//如果碰撞双方均为子弹,不发生碰撞。 23 return; 24 } 25 super_onConflict(other); 26 } 27 that.isbullet=function(){ 28 return true;//该方法仅仅表明这个类是子弹类。。。 29 }; 30 return that; 31 }; 32 var playerbullet=function(spec){//选手子弹类,发出子弹就新给一个子弹类 33 var that=bullets(spec);//子弹类继承bullets类,好多方法父类都实现了,子类就改个onMove方法就行了 34 35 that.onMove=function(){ 36 if(that.y()<0||that.y()>CANVAS_HEIGHT||that.x()<0||that.x()>CANVAS_WIDTH){//这里只判定如果超出边界,就让它消失,其他的方法父类已经实现了。 37 //alert("outbundle"+that.x()+that.y()); 38 //alert(that.index()); 39 return [{func:"disapear",params:[that.index()]},{func:"reduceBulet",params:[1]}]; 40 41 } 42 //that.judgeBundle(); 43 that.move(0,-1);//移动垂直向上 44 45 return {type:"drawimg",func:"drawimg",params:[that.img(),that.x(),that.y()]};//如果没超出边界,更新它在画布上的位置 46 47 }; 48 return that;//这个变量就是父类的引用。子类可以对父类进行扩展,就是通过这样的方式。 49 };
friendplane.js:
//这个全局变量用来响应按键,上下左右,改变它的私有变量,并具有返回私有变量的方法。外部不可访问其私有变量。
$myplane=function(){
var movex=0;//x轴的方向
var movey=0;//y轴方向
//var status=0;
$(document).bind('keydown',function(event){//按键按下事件
if(event.which){
switch (event.which) {
case 37://左边按下
movex=-1;
//movey=0;
status=0;
break;
case 38://上边按下
movey=-1;
//status=0;
break;
case 39://右边
movex=1;
//movey=0;
//status=0;
break;
case 40://下边
//movex=0;
movey=1;
//status=0;
break;
//case 17://control按下(先不处理)
//status=1;
//break;
default: //默认
movex=0;
movey=0;
status=0;
break;
}
}
});
//键盘按上事件
$(document).bind('keyup',function(event){
if(event.which){
switch (event.which) {
case 37://左边
case 39://右边
movex=0;
break;
case 38://上边
case 40://下边
movey=0;
break;
default: //默认
break;
}
}
});
return {
init:function(){
movex=0;
movey=0;
status=0;
},
x:function(){
return movex;
},
y:function(){
return movey;
},
status:function(){
return status;
}
};
}();
var playerplane=function(spec){//选手飞机
var shootTimes=0;//这个用来做频率的次数统计,多少帧发一个子弹,后面会给它递增,然后求模,然后又初始化为1
var frq=spec.bullet.frq;//如果子弹中有频率字面量,那么这个频率能确定,否则给一个不会看花眼的频率
var frqcy=Math.floor((typeof frq === 'undefined')?20:frq);
if(typeof Object.beget != 'function'){//这个步骤完成了Object的beget方法,新建立Object就不用new Object()了,那简直弱爆了,只要调用Object.beget('xxx')就直接实
//例化了,又复制了'xxx'这个类的所有的键值对。这个方法得自《javascript语言精粹》一书
Object.beget=function(o){
var F=function(){};
F.prototype=o;
return new F();
};
}
//var specblt=Object.beget(spec.bullet);
//var specblt=spec.bullet;
spec.x=0;//选手飞机的横坐标初始为0,或者任意值
spec.y=CANVAS_HEIGHT;//选手飞机的纵坐标为最下面
var that=fly(spec);//选手也是fly的子类。
that.shoot=function(){//不过选手能发炮弹,fly就不可以
var specblt=Object.beget(spec.bullet);//这个是调用了Object刚刚初始化的原型,复制一个bullet的实例
specblt.x=that.x()+(that.width()-specblt.img.width)/2;//然后bullet最好是在正中央发出去,当然如果你不想这么做,也没关系。
specblt.y=that.y()-(that.height()-specblt.img.height)/2;
return {func:"shoot",params:[specblt]};//给个发射事件交给后台去处理吧。
};
that.onMove=function(x,y){//选手飞机不需要来回跳来跳去的,只要根据键盘响应进行移动就好啦,还是调用了move方法。
shootTimes++;
if(shootTimes==frqcy)
{
shootTimes=1;
}
that.move($myplane.x(),$myplane.y());
that.judgeBundle();
//$myplane.init();
//alert(shootTimes%frqcy);
if(shootTimes%frqcy === 1){
return [{type:"drawimg",func:"drawimg",params:[that.img(),that.x(),that.y()]},that.shoot()];//如果这个帧已经过了预定的次数,就发射一颗炮弹并根据需求改变自己的位置
}
return {type:"drawimg",func:"drawimg",params:[that.img(),that.x(),that.y()]};//否则就改变自己的位置就好啦。
};
return that;
};
注意:这里的对象实例化形式是:
var objct = function(spec){//spec为私有变量不可全局访问因此这种方式有良好的封装性 return {//此处返回了一个对象,对象中的内容以字面量:属性值的形式 oo:function(){ return spec.oo; }, xx:function(){ return spec.xx; } }; };
我们得到父类方法的引用的形式是:
Function.prototype.method = function(name,func){//为Function增加method,调用模式为:function1.method('xxx',function(){}); if(!this.prototype[name]) { this.prototype[name] = func; } return this; } Object.method('superior',function(name){//调用上面的method方法,给Object增加父类的内容。 var that=this; var method = that[name]; return function(){ return method.apply(that,arguments);//第一个参数为上下文,第二个参数为传递的参数。 }; }); var super_onConflict=that.superior('onConflict');//父类的onConflict方法
这种形式是书上写的,对于这种形式,我有些不懂得地方:我们在Function的原型中链接了一个method方法,又在Object调用method方法那么按照书上所说:“Function的prototype(原型链)是隐藏链接到Object的,而javascript在查找原型时,是一级一级查找的,如果当前级别的原型字面量可以找到就返回这个字面量对应的值,否则就根据原型链查找上一级的原型直到找到为止,而如果在Object中都找不到原型就返回'undefined'”这里我在Object中岂不是根本就找不到method这个字面量对应的内容了?这岂不是相互矛盾?而程序却明显没有报错。希望有人能够解我困惑,指我迷津。
有关这种形式,推荐一本书:《javascript语言精粹》(Douglas Crockford著,赵泽欣译)后面会给出一些此书中自己认为比较重要的笔记
恩,Model层就说到这里罢,这个Model层是可以扩展的,只要按照bullets.js的形式,可以有无数个子类的哦。根据抽丝剥茧的原理,下一篇将介绍打灰机service层的实现。