HTML5入门习作2:炮台

上一篇中,初学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则游戏结束并停止动画。此时会添加一个鼠标点击事件,点击后重新开始并移除此事件,以免再次误击。

这些说明可能有些粗略,不过对阅读代码还是有帮助的。尽管我在边开发边修改时没有进行进一步的封装和面向对象化,但按顺序阅读可能会更简单些。

下一作品预计改进的地方:

  • 使用离屏画布,避免把运算和呈现放在一起;
  • 解决更高速下的碰撞检测(即两个对象已经飞过了碰撞点,但两帧之间应该有过碰撞),如果离屏画布的帧速仍然不足以解决,只好考虑计算两线段的最小间距了;
  • 更方便使用的颜色和透明遮罩算法;
  • 贴图而不仅仅使用几何图形;
  • 适当将对象特性封装起来,而不是完全在主程序中写循环算法。

以及一点开发感想:

程序写起来不是难事,但数值调整真的很不容易……就这么几个简单参数,包括炮台数量、子弹发射速度、间隔、目标入场数量等等,我调了十几次才勉强达到“既不被秒杀,又不会长生不死”的地步,而且也不很满意。游戏设计很不容易啊。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注