上一篇中,初学html5动画的我在canvas画布上用彩色小球模拟了烟花喷射的效果,功能虽简单,但在制作中涉及到了几个要点:计算、绘制、显示。而且实现了可兼容旧版的requestAnimationFrame方法,以达到更平滑的动画效果。
接下来,我准备在上一篇的基础上加以改进,实现一个有简单互动的小游戏。
当然,只有小球是远远不够用的,在边调试边开发的过程中,下列功能也逐一实现:
封装动画框架到单独js文件(代码复用);
生成多边形(绘制炮台);
渐变填充(绘制防卫区);
角度计算(发射扇面形散布的炮弹);
碰撞检测(炮弹击中目标,以及目标撞墙后的反弹);
鼠标事件(实时调整炮口方向);
以及之前实现过的渐隐对象,用于显示摧毁后的爆炸效果、全屏闪烁以及文字提示。
那么,先看看效果再讲解代码吧。
操作提示:
鼠标控制炮口方向,炮弹自动发射;
击坠10、20、40、80、160……个目标时会升级,升级后某个炮台威力会增加,相应的,敌人数量也会增加;
上方中央的“@”表示HP,敌人触底一次则减少一格;
GameOver后,点击屏幕重新开始。
(使用移动设备的朋友可能占了便宜,因为手机的竖屏玩起来炮火比较集中)
OK,下面是解说时间。由于代码较长,就不全贴出来了,可以右击上面的iframe直接查看源码。
首先是动画框架。在上一篇中,下一帧的请求、帧数计算、时间统计等等都和主程序放在了一起,容易混淆。所以我把它单独抽到一个anime.js里。
里面有两个主要方法,一个是外部用于建立动画对象的createAnime(),其参数为回调函数;另一个是内部执行动画帧的_animeFrame()。另外还有一系列动画状态、帧数等参数。
这些操作的封装避免了主程序和动画控制产生混淆。使用时,只需执行createAnime()建立一个动画对象,然后调用它的start()方法即可开始运行,调用stop()则停止;每帧自动执行回调函数时会传递两个参数,分别是此帧之前经过的时间,以及上一秒帧数,供主程序使用。
//建立动画对象
function createAnime(callback) {
if (!callback) return null;
var anime = new Object();
anime.starttime//动画开始时间
anime.fps;//上一秒帧数
anime.secondstarttime;//当前秒开始时间(用于统计每秒的帧数)
anime.framestarttime;
anime.nextfps;
anime.status = "idle";
anime.start = function () {
if (this.status == "running") return;
this.status = "running";
if (this.status == "idle") this.starttime = new Date();//初次启动才计时
this.secondstarttime = new Date();
this.framestarttime = new Date();
this.fps = 0;//上一秒帧数
this.nextfps = 0;
_animeFrame(this, callback);
};
anime.stop = function () {
this.status = "stopped";
};
return anime;
}
function _animeFrame(anime, callback) {
anime.nextfps++;//累加帧数
var currenttime = new Date()
if (currenttime - anime.secondstarttime >= 1000) {//满1秒则为帧数赋值,清空计数
anime.fps = anime.nextfps;
anime.nextfps = 0;
anime.secondstarttime = currenttime;
}
var framespan = currenttime - anime.framestarttime;//计算两帧间隔
anime.framestarttime = currenttime;
callback(framespan, anime.fps);
if (anime && anime.status == "running") {
requestAnimationFrame(function () {
_animeFrame(anime, callback);
}
);
}
}
当然,之前处理requestAnimationFrame兼容性那段代码也要加上。
说完了框架,接下来就是炮台程序本身了。
其实主题结构和之前演示烟花的基本类似,只是每帧动画时处理的内容多了些。
首先是init()初始化代码,设置画布尺寸,并使用画布长宽值的较小者作为基准尺寸basesize,此值十分重要,后面的目标、子弹、炮台等各物体的尺寸及大小均与此值成比例;
初始化之后,生成动画对象并执行start()开始,每一帧主要处理下面这些对象数组:
var cannons = new Array();//炮台
var bullets = new Array();//子弹
var targets = new Array();//射击目标
var blasts = new Array();//爆炸的目标
var messages = new Array();//消息对象
除了炮台数量为固定的(根据画面的长宽比决定炮台数量)之外,其余对象数组在每帧中都可能有所变化。所以对每个数组都采取了类似下列方式的处理:
var newbullets = new Array();
for(var i in bullets){
...
if (...) newbullets.push(bullets[i]);
}
bullets = newbullets;
即添加可以留到下一帧的对象,然后交换新旧数组。
各数组间的逻辑关系是这样的:
炮台(cannon)根据所指的方向和指定时间间隔,以扇形角度发射指定数量子弹(bullet);
子弹沿指定方向和速度飞行,如飞出画布则移除;子循环遍历各个目标(target),如发生碰撞(两者中心距离小于两者半径之和)则移除子弹,并设置目标为击中状态;
击中数达到指定次数则升级并显示消息(Message);
遍历目标数组。如击中,则移除目标并在原地添加一个爆炸(blast);如未击中则继续飞行,碰到左右边框则反弹,碰到底边框的话也产生爆炸并扣HP,同时也显示警告消息;
各爆炸随时间推移会变大并变淡,淡至透明则移除;
显示当前消息,消息文字在中间出现,慢慢向上移动并变淡,淡至透明则移除;
等待下一帧。
以上每一步都是计算和绘制交叉进行;
实际代码的顺序与上文描述并不一致,首先遍历的是爆炸,然后是目标和子弹,最后是炮台和消息,这是为了保持图层绘制时的上下关系;
画布绑定鼠标mousemove事件,获取鼠标坐标,并通过计算使所有炮台瞄准此坐标点;
HP减至0则游戏结束并停止动画。此时会添加一个鼠标点击事件,点击后重新开始并移除此事件,以免再次误击。
这些说明可能有些粗略,不过对阅读代码还是有帮助的。尽管我在边开发边修改时没有进行进一步的封装和面向对象化,但按顺序阅读可能会更简单些。
下一作品预计改进的地方:
使用离屏画布,避免把运算和呈现放在一起;
解决更高速下的碰撞检测(即两个对象已经飞过了碰撞点,但两帧之间应该有过碰撞),如果离屏画布的帧速仍然不足以解决,只好考虑计算两线段的最小间距了;
更方便使用的颜色和透明遮罩算法;
贴图而不仅仅使用几何图形;
适当将对象特性封装起来,而不是完全在主程序中写循环算法。
以及一点开发感想:
程序写起来不是难事,但数值调整真的很不容易……就这么几个简单参数,包括炮台数量、子弹发射速度、间隔、目标入场数量等等,我调了十几次才勉强达到“既不被秒杀,又不会长生不死”的地步,而且也不很满意。游戏设计很不容易啊。
2+