上一篇中,初学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则游戏结束并停止动画。此时会添加一个鼠标点击事件,点击后重新开始并移除此事件,以免再次误击。
这些说明可能有些粗略,不过对阅读代码还是有帮助的。尽管我在边开发边修改时没有进行进一步的封装和面向对象化,但按顺序阅读可能会更简单些。
下一作品预计改进的地方:
- 使用离屏画布,避免把运算和呈现放在一起;
- 解决更高速下的碰撞检测(即两个对象已经飞过了碰撞点,但两帧之间应该有过碰撞),如果离屏画布的帧速仍然不足以解决,只好考虑计算两线段的最小间距了;
- 更方便使用的颜色和透明遮罩算法;
- 贴图而不仅仅使用几何图形;
- 适当将对象特性封装起来,而不是完全在主程序中写循环算法。
以及一点开发感想:
程序写起来不是难事,但数值调整真的很不容易……就这么几个简单参数,包括炮台数量、子弹发射速度、间隔、目标入场数量等等,我调了十几次才勉强达到“既不被秒杀,又不会长生不死”的地步,而且也不很满意。游戏设计很不容易啊。