HTML5学习+javascript学习:打飞机游戏简介以及Model层

时间:2023-03-09 06:05:23
HTML5学习+javascript学习:打飞机游戏简介以及Model层

本着好记性不如烂博客以及分享成功的喜悦和分享失败的苦楚,今天我来分享下一个练手项目:打飞机游戏~从小就自己想做游戏,可是一直没有机会。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

HTML5学习+javascript学习:打飞机游戏简介以及Model层

这里我们先给出fly和bullet的代码,其他的就自己扩展

fly.js:

 /*飞行物类,不管敌机,boss,己机,还是子弹,都是飞行物,它们都有改变位置,碰撞,越界判定的方法,也都有碰撞体积,图像,坐标,序号和hp等属性。具体参数如下:
*var spec={
x:1,画布中的横坐标
y:CANVAS_HEIGHT-5,//画布中的纵坐标
hp:1,//飞行物的血量
index:svc.total(),//序列号
exploreImg:getImg("img/blasts3.png"),//爆炸的图片
img:getImg("img/mybullet2.png"),//自身图片
target:0,//目标
conflictSquare:20,//碰撞体积
speedX:5,//X方向的速度
speedY:2,//Y方向的速度
movex:0,//X轴移动的方向
movey:0//Y轴移动的方向
};
*这个对象完全可以用json来代替!可扩展性就不多说了。
*它大多数方法(除了对私有变量访问的方法)均返回数组格式为:[{func:'',params:[]},{func:'',params:[]}]看到了什么?很像json吧?不过不是用json来调用的,
*它是在控制层(service)接收,然后把有用的信息注册到一个array中来管理,根据func这个字段的不同,调用不同的方法。对象有index,删除的时候就可以很方便的删掉了。
*整体来说,这个fly类是所有用到的模型的父类,
*这种以方法(function)来构建对象的形式是安全地,因为它返回的是有很多属性的对象(return{a:function(){},b:function(){},,,,};),对象的所有内容均可访问其私有变量,
*但是脚本不能在外部访问其自身私有变量。这对于透明性和扩展性都是有好处的。
*/
var fly=function(spec){ var imgWidth=spec.img.width;//图片的宽度,该变量是私有变量,当fly这个函数完蛋了,它的私有变量仍然可以访问,神奇吧,这就是javascript的魅力。
var imgHeight=spec.img.height;//图片的高度
return {
move:function(directionX,directionY){//根据方向来移动0,1表示Y轴向下。1,0表示X轴向右,依次类推 spec.x=(directionX||0)*spec.speedX+spec.x;//水平移动,任何移动的类型,只要不要太变态,应该都能调用这个方法来改变坐标,如果有不能调用的再说。 spec.y=(directionY||0)*spec.speedY+spec.y;//垂直移动,其他同上。 },
hpChange:function(newHp){//碰撞了,血量改变了或者飞机挂掉了就要用到这个方法改变私有变量的值
spec.hp=newHp;
},
x:function(){//得到画布中的x坐标
return spec.x;
},
y:function(){//得到画布中的y坐标
return spec.y;
},
hp:function(){//血量,访问私有属性血量要用到这个方法
return spec.hp;
},
index:function(){//这个序列属性是用来删除自己的,使用array的splice方法,然后我们再做些手脚,就可以让index和array的序列相同啦。
return spec.index;
},
setIndex:function(ndex){//这个方法用来改变自身的序列
spec.index=ndex;
},
exploreImg:function(){//得到爆炸的图片,如果爆炸事件发生,图片被缓存,之后与其他内容一起画到画布上去。
return spec.exploreImg;
},
img:function(){//得到自身图片,之后在控制层(service)缓存
return spec.img;
},
target:function(){//得到敌对目标的编号,敌飞行物和友飞行物当然不能一样了。
return spec.target;
},
width:function(){//图片的宽度
return spec.img.width;
},
height:function(){//图片的高度
return spec.img.height;
},
cflctSqr:function(){//碰撞体积(是一个正方形,这个方法得到碰撞体积的边长)
if(spec.conflictSquare){
return spec.conflictSquare;
}
return 0;
},
onConflict:function(other){//事件,后台判断碰撞,当碰撞发生时就调用这个方法,返回一个事件交给控制层处理。
//alert(spec.hp+" "+other.hp());
if((spec.hp>0) && (other.hp()>0)){//如果碰撞接收者和碰撞发出者的hp属性都大于零,才能进行碰撞,碰撞的结果就是它们的血值相减
//这又造成了有一个血值变为了零或负,或者两者同归于尽
//dispear 这个事件代表消失,即如果一个飞行物被干掉了,我就让后台来处理这个被干掉的家伙的后事,把它从生龙活虎的队列中删除~
//disapear函数可以接受多个参数,这个参数是一个数组,数组包含多个消失的对象(当然一般都是只会消失一个对象)。 var hpo=other.hp();//hpo就是传入的参数的hp,两个飞行物碰撞,就假设一个是主动者,这样好分析点。
var hps=spec.hp;//hps是被碰的hp
spec.hp -= hpo;//被碰的hp减去碰撞的hp
other.hpChange(other.hp()-hps);//碰撞的hp再减去被碰的hp这两件事造成了如下的代码来判断碰撞双方的状态
var returnArray=[];//返回一个事件的集合先给他初始化为一个数组
//alert(spec.hp+" "+other.hp());
if(spec.hp == 0 && other.hp() == 0){//如果同归于尽,两者都爆炸,两者都消失
returnArray=[];
returnArray.push({func:"explore",params:[other.exploreImg(),other.x(),other.y()]});//碰撞发起的爆炸事件
returnArray.push({func:"explore",params:[spec.exploreImg,spec.x,spec.y]});//碰撞接受者的爆炸事件
returnArray.push({func:"disapear",params:[spec.index]});//碰撞接收者的消失事件入栈
returnArray.push({func:"disapear",params:[other.index()]});//碰撞发起者的消失事件入栈 return returnArray; }else if((spec.hp <=0) && (other.hp()>0)){//如果碰撞发起者它的hp远比接受者要大,那么接收者爆炸,消失
returnArray=[];
returnArray.push({func:"explore",params:[spec.exploreImg,spec.x,spec.y]});
returnArray.push({func:"disapear",params:[spec.index]});
return returnArray; }else if((spec.hp > 0) && (other.hp()<=0)){//如果碰撞发起者它的hp小于接受者,那么碰撞发起者爆炸,消失。
returnArray=[];
returnArray.push({func:"explore",params:[other.exploreImg(),other.x(),other.y()]});
returnArray.push({func:"disapear",params:[other.index()]});
return returnArray;
}else{//这种情况我不知道为什么可以发生,因此不打算处理这种情况,但为了保险起见返回一个undefined。
return ;
}
} },
judgeBundle:function(){//超出边界把它揪回来。 if(spec.x<=0){//如果横坐标超出了左边的界限,揪回来。
spec.x=0;
}else if(spec.x>=(CANVAS_WIDTH-imgWidth)){//如果横坐标大于画布边界与图片边界的差就是到了边缘 spec.x=CANVAS_WIDTH-imgWidth;//让它等于边界的值 }
if(spec.y<=0){//和x轴类同。
spec.y=0;
}else if(spec.y>=CANVAS_HEIGHT-imgHeight){
spec.y=CANVAS_HEIGHT-imgHeight;
}
},
onMove:function(){//onMove:移动方法->judgeBundle->move,此处只是循环的移动。,当然也可以自己定义移动方法,但要在子类中定义了。 if(spec.y>CANVAS_HEIGHT-imgHeight||spec.y<0){//如果到了最上边,或者最下边就向相反方向移动
spec.movey=-spec.movey; }
if(spec.x>(CANVAS_WIDTH-imgWidth)||spec.x<0){//类似于上面的判断
spec.movex=-spec.movex; }
this.judgeBundle();//判断是否超出边界
this.move(spec.movex,spec.movey);//移动 return {type:"drawimg",func:"drawimg",params:[spec.img,spec.x,spec.y]};//移动了,把移动事件返回去,缓存图片和坐标,最后一起画出来。
}
};
};

fly.js

bullets.js:

 /*子弹类,继承了fly类,增加了Function类的method方法,给Object增加了superior方法使其可以调用父类的方法。,增加了isbullet方法。
*改写了onConflict方法使子弹之间不能发生碰撞。
*/
var bullets=function(spec){
var isbullet=true;//是否子弹(父类没有)
var that=fly(spec);//子弹类继承fly类
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方法
that.onConflict=function(other){
if(typeof other.isbullet === 'function' && other.isbullet()){//如果碰撞双方均为子弹,不发生碰撞。
return;
}
super_onConflict(other);
}
that.isbullet=function(){
return true;//该方法仅仅表明这个类是子弹类。。。
};
return that;
};
var playerbullet=function(spec){//选手子弹类,发出子弹就新给一个子弹类
var that=bullets(spec);//子弹类继承bullets类,好多方法父类都实现了,子类就改个onMove方法就行了 that.onMove=function(){
if(that.y()<0||that.y()>CANVAS_HEIGHT||that.x()<0||that.x()>CANVAS_WIDTH){//这里只判定如果超出边界,就让它消失,其他的方法父类已经实现了。
//alert("outbundle"+that.x()+that.y());
//alert(that.index());
return [{func:"disapear",params:[that.index()]},{func:"reduceBulet",params:[1]}]; }
//that.judgeBundle();
that.move(0,-1);//移动垂直向上 return {type:"drawimg",func:"drawimg",params:[that.img(),that.x(),that.y()]};//如果没超出边界,更新它在画布上的位置 };
return that;//这个变量就是父类的引用。子类可以对父类进行扩展,就是通过这样的方式。
};

bullets.js

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;
};

friendplane.js

注意:这里的对象实例化形式是:

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层的实现。