【CSON原创】HTML5游戏框架cnGameJS开发实录(动画篇)
在游戏中,游戏角色的动画效果是一个游戏必不可少的一部分。这节我们以构造超级马里奥的角色为例,讲解cnGameJS里动画的实现。
1.原理:
一个动画如果要实现一连串动作,我们可以把每个动作的快照保留起来,并放在一个大图上面,然后每次帧更新的时候,就在每个动作的快照之间循环显示,最终得出一个动画。因此我们首先要准备一个类似下面的这种图片:
看到不?把每个动作放在图片的不同位置上,之后就可以通过改变显示位置实现动画效果了。
当cnGameJS调用start方法开始游戏后,将会调用传入的gameObj的initialize方法进行初始化,并且生成一个游戏循环,循环里每次调用gameObj的update和draw方法。(详见(HTML5游戏框架cnGameJS开发实录(资源加载篇)和 HTML5游戏框架cnGameJS开发实录(游戏循环篇)))因此我们可以把动画的初始化放在gameObj的initialize中,update和draw分别放在gameObj的update和draw中,实现动画播放。
效果:
代码:
<body>
<canvas id="gameCanvas">请使用支持canvas的浏览器查看</canvas>
</body>
<script src="https://files.cnblogs.com/Cson/cnGame_v1.0.js"></script>
<script>
var Src="http://pic002.cnblogs.com/images/2012/273330/2012021312050269.png";
/* 初始化 */
cnGame.init('gameCanvas',{width:50,height:60});
var gameObj={
initialize:function(){
this.marie=cnGame.SpriteSheet("marie",Src,{frameSize:[50,60],width:150,height:60,loop:true});
},
update:function(){
this.marie.update();
},
draw:function(){
this.marie.draw();
}
}
cnGame.loader.start([Src],gameObj);
</script>
2.实现
正如上面看到的,我们只需要用很少的代码量,就可以实现一个帧动画的播放,接下来将介绍cnGameJS里的帧动画是怎样封装的。
大家很容易可以发现,cnGameJS都遵循一个特定的模式,把对象的阶段分为三个:initialize(初始化),update(帧更新)和draw(绘制)。这样我们可以很方便地把不同功能的代码写在对应的阶段内。spriteSheet帧动画也不例外,同样按照这种模式来写。
初始化:用户对一些必要的信息进行设定。
spriteSheet.prototype={
/**
*初始化
**/
init:function(id,src,options){
/**
*默认对象
**/
var defaultObj={
x:0,
y:0,
width:120,
height:40,
frameSize:[40,40],
frameDuration:100,
direction:"right", //从左到右
beginX:0,
beginY:0,
loop:false,
bounce:false
};
options=options||{};
options=cg.core.extend(defaultObj,options);
this.id=id; //spriteSheet的id
this.src=src; //图片地址
this.x=options.x; //动画X位置
this.y=options.y; //动画Y位置
this.width=options.width; //图片的宽度
this.height=options.height; //图片的高度
this.image=cg.loader.loadedImgs[this.src]; //图片对象
this.frameSize=options.frameSize; //每帧尺寸
this.frameDuration=options.frameDuration; //每帧持续时间
this.direction=options.direction; //读取帧的方向(从做到右或从上到下)
this.currentIndex=0; //目前帧索引
this.beginX=options.beginX; //截取图片的起始位置X
this.beginY=options.beginY; //截图图片的起始位置Y
this.loop=options.loop; //是否循环播放
this.bounce=options.bounce; //是否往返播放
this.onFinsh=options.onFinsh; //播放完毕后的回调函数
this.frames=caculateFrames(options); //帧信息集合
this.now=new Date().getTime(); //当前时间
this.last=new Date().getTime(); //上一帧开始时间
},
上面的参数比较多,都是一些对帧动画属性的预设置。需要注意的是我们调用了私有方法caculateFrames来计算每个帧的信息,并保存到frames内,为帧绘制做准备。
帧更新:
在每一帧的更新过程中,我们首先获取当前时间作为帧的开始时间,并且和上一次帧的开始时间相减,就得出上一次帧的用时。如果用时超过之前设置的每帧的用时,则可以进行帧更新。然后判断是否循环或者往返播放动画,按情况更新对应的帧索引。在最终确定帧的索引后,就可以从frames数组中获取该帧的信息,并返回。
/**
*更新帧
**/
update:function(){
this.now=new Date().getTime();
var frames=this.frames;
if((this.now-this.last)>this.frameDuration){//如果间隔大于帧间间隔,则update
var currentIndex=this.currentIndex;
var length=this.frames.length;
this.last=this.now;
if(currentIndex>=length-1){
if(this.loop){ //循环
return frames[this.currentIndex=0];
}
else if(!this.bounce){//没有循环并且没有往返滚动,则停止在最后一帧
this.onFinsh&&this.onFinsh();
this.onFinsh=undefined;
return frames[currentIndex];
}
}
if((this.bounce)&&((currentIndex>=length-1&&path>0)||(currentIndex<=0&&path<0))){ //往返
path*=(-1);
}
this.currentIndex+=path;
}
return frames[this.currentIndex];
},
帧绘制:
在帧更新后,已经获取到当前帧的索引,因此draw方法就可以从保存所有帧信息的frames获取到当前帧的信息(包括图像截取的起始位置等),从而在指定位置截取大图片,并画出该图片区域的图像:
/**
*在特定位置绘制该帧
**/
draw:function(){
var currentFrame=this.getCurrentFrame();
var width=this.frameSize[0];
var height=this.frameSize[1];
cg.context.drawImage(this.image,currentFrame.x,currentFrame.y,width,height,this.x,this.y,width,height);
}
最后,还提供跳到特定帧等方法。
动画模块所有源码:
/**
*包含多帧图像的大图片
**/
spriteSheet=function(id,src,options){
if(!(this instanceof arguments.callee)){
return new arguments.callee(id,src,options);
}
this.init(id,src,options);
}
spriteSheet.prototype={
/**
*初始化
**/
init:function(id,src,options){
/**
*默认对象
**/
var defaultObj={
x:0,
y:0,
width:120,
height:40,
frameSize:[40,40],
frameDuration:100,
direction:"right", //从左到右
beginX:0,
beginY:0,
loop:false,
bounce:false
};
options=options||{};
options=cg.core.extend(defaultObj,options);
this.id=id; //spriteSheet的id
this.src=src; //图片地址
this.x=options.x; //动画X位置
this.y=options.y; //动画Y位置
this.width=options.width; //图片的宽度
this.height=options.height; //图片的高度
this.image=cg.loader.loadedImgs[this.src]; //图片对象
this.frameSize=options.frameSize; //每帧尺寸
this.frameDuration=options.frameDuration; //每帧持续时间
this.direction=options.direction; //读取帧的方向(从做到右或从上到下)
this.currentIndex=0; //目前帧索引
this.beginX=options.beginX; //截取图片的起始位置X
this.beginY=options.beginY; //截图图片的起始位置Y
this.loop=options.loop; //是否循环播放
this.bounce=options.bounce; //是否往返播放
this.onFinsh=options.onFinsh; //播放完毕后的回调函数
this.frames=caculateFrames(options); //帧信息集合
this.now=new Date().getTime(); //当前时间
this.last=new Date().getTime(); //上一帧开始时间
},
/**
*更新帧
**/
update:function(){
this.now=new Date().getTime();
var frames=this.frames;
if((this.now-this.last)>this.frameDuration){//如果间隔大于帧间间隔,则update
var currentIndex=this.currentIndex;
var length=this.frames.length;
this.last=this.now;
if(currentIndex>=length-1){
if(this.loop){ //循环
return frames[this.currentIndex=0];
}
else if(!this.bounce){//没有循环并且没有往返滚动,则停止在最后一帧
this.onFinsh&&this.onFinsh();
this.onFinsh=undefined;
return frames[currentIndex];
}
}
if((this.bounce)&&((currentIndex>=length-1&&path>0)||(currentIndex<=0&&path<0))){ //往返
path*=(-1);
}
this.currentIndex+=path;
}
return frames[this.currentIndex];
},
/**
*跳到特定帧
**/
index:function(index){
this.currentIndex=index;
return this.frames[this.currentIndex];
},
/**
*获取现时帧
**/
getCurrentFrame:function(){
return this.frames[this.currentIndex];
},
/**
*在特定位置绘制该帧
**/
draw:function(){
var currentFrame=this.getCurrentFrame();
var width=this.frameSize[0];
var height=this.frameSize[1];
cg.context.drawImage(this.image,currentFrame.x,currentFrame.y,width,height,this.x,this.y,width,height);
}
}
this.SpriteSheet=spriteSheet;
});