17、《每周一点canvas动画》——星球守护

代码文件

在介绍完碰撞检测的内容后,总感觉不拿它做点什么事情有点虚啊!本章的内容应该在上周就更新完成,但是由于这几天实在比较忙。一直没空把这个小游戏的过程写出来,就一直拖到了现在。如题,我取了个比较炫酷的名字——星球守护。其实呢,也就是一个简单的射击小游戏。作者并不是我,具体的效果可以去这里体验。我对代码做了部分修改与注释,并且添加了爆炸音效,和游戏结束音效。

ok!接下来我们就来一步一步的介绍这个游戏完成过程。

1.文件结构及静态资源介绍

文件结构简单到令人发指

--css                   //css文件
--imgs                  //图片文件
--js                    //game.js文件
--media                 //音频文件
--index.html            //主文件

我们用到的图片资源比较少,总共两幅雪碧图,我们现在来介绍第一幅。

这幅图包括了我们游戏中要用到的所有元素,星球,飞机, 子弹, 陨石,以及控制按钮。另一幅,是爆炸的雪碧图,我就不列出来了,把文件下下来看看就好。

主文件和样式文件都很简单,在这我就不做过多介绍了,我们直接开始我们呢的游戏流程。

2.游戏流程分析

仔细观察上面的动图,我们大致的可以把游戏分为三部分:开始前游戏进行中游戏结束

在开始前我们的页面主要呈现:星球, 飞机, 和开始按钮。此时当鼠标在canvas画布上滑动时,飞机是不会做出任何反应,因为此时游戏还没开始。唯一运动的东西是一个自转的星球。

当我们点击了开始按钮以后,游戏开始。这时候

  1. 在左上角会有个record用于记录游戏的最高纪录,如果你是第一次玩它就是0。

  2. 飞机会跟随我们的鼠标方向做旋转(似成相识吧,是不是我们的鼠标跟随动画)。

  3. 在星球的最中间,开始按钮消失,中间的数字用于实时计算我们消灭的陨石数量。

  4. 从屏幕的四周会随机的出现大小不一的陨石

  5. 点击鼠标发射子弹,基于距离做碰撞检测。如果发生碰撞,则画爆炸的雪碧图。

  6. 当陨石距离星球一定距离游戏结束(这里也是用基于距离的碰撞检测)

游戏结束这个界面主要有重新开始按钮,本次游戏总共消灭的陨石,和记录。

3.初始化

首先把资源文件加载进来,这里我们为window对象添加了DOMContentLoaded事件,当内容完全加载完毕,执行game函数

(function(){
    window.addEventListener("DOMContentLoaded", game);

    //加载雪碧图&&爆炸图片
    var sprite = new Image();
    var spriteExplosion = new Image();

    window.onload = function(){
        sprite.src = "imgs/sprite.png";
        spriteExplosion.src = "imgs/explosion.png";
    }

    function game(){
     //一堆函数
    }
})()

在game函数中,我们先初始化一些变量

var canvas = doplaybackRate = document.getElementById('canvas'),
    ctx = canvas.getContext('2d');

var W = canvas.width = window.innerWidth,
    H = canvas.height = window.innerHeight;

var bullets   = [], //子弹
    asteroids = [], //陨石
    explosions = [], //爆炸
    destroyed = 0, //总的摧毁数
    record    = 0, //记录
    count     = 0,//用于记录游戏中实时的摧毁数目
    playing   = false, //游戏状态
    gameover  = false, //用于判断游戏是否结束
    _planet   = {deg: 0}; //行星角度

//飞机初始位置
var player = {
            posX  : -35,
            posY  : -(100 + 82),
            width : 70,
            height: 79,
            deg   : 0
        };

变量初始化完成后,就开始我们真正的逻辑部分了,我们这里先看一下在game中包括哪些函数。

//判断鼠标的点击位置,判断游戏是否开始
function action(e){...}

//画星球
function planet(){...}

//画飞船
function _player(){...}

//发射子弹
function fire(){...}

//飞船转动角度
function move(e){...}

//初始化&&创建陨石
function newAsteroid(){...}

//陨石运动
function _asteroids(){...}

//爆炸
function explosion(asteroid){...}

//启动函数
function start(){...}

//动画循环
(function drawFrame(){
    window.requestAnimationFrame(drawFrame,canvas);
    start();
}())

4.画星球

画星球的函数非常简单

function planet(){
            var rotateSpeed = 0.1; //星球的旋转速度
            ctx.save();
            ctx.translate(W/2, H/2); //置于画布中心

            //每一帧都加上rotateSpeed
            ctx.rotate((_planet.deg += rotateSpeed)*Math.PI/180);

            //画雪碧图中的星球
            ctx.drawImage(sprite, 0, 0, 200, 200, -100, -100, 200, 200);
            ctx.restore();
        }

这里你要知道drawImage这个方法中的各项参数

draw(image,sx, sy, sw, sh, dx, dy, dw, dh)

sx,sy为物体在雪碧图中的位置,sw,sh是在雪碧图中的宽高,s可以理解为source(源)。
dx,dy为物体在canvas画布中的位置, dw,dh是在canvas画布中的宽高, d可以理解为destination(目的地),后面的其他图形的绘制也都遵循这个方法。

5.画飞机

function _player(){
            ctx.save();
            ctx.translate(W/2, H/2);
            ctx.rotate(player.deg);
            ctx.drawImage(sprite, 200, 0, player.width, player.height, player.posX, player.posY, player.width, player.height);
            ctx.restore();

            ...
}

在初始化的时候,我们设定了飞船的一些初始状态,我们调用这些属性,将其画在canvas画布中,这时候你可以把这两个函数放在动画循环中你可以看到星球与飞船已经画出来了,但是,到现在为止还是不会对我们的鼠标做出任何反应。下一步,我们就让飞船可以跟随我们的鼠标做出相应的转动。

6.交互逻辑


action()这个函数中,主要做的是整个canvas画布与用户的交互。通过变量playing来控制游戏是否在进行。如果,palyingtrue,就是游戏正在进行,那么每点击一次我们就往bullets数组里填充一颗子弹。如果,playingfalse, 就说明游戏没有在进行,那么可能有两种情况游戏还没开始,或是游戏已经结束。

我们先设定一个变量dist,它的作用是判断鼠标的点击位置。仔细观察游戏你会发现,开始按钮位于星球的中间,游戏gameover后重新开始的图标也位于星球的中心。

所以,在这里要用到另一个变量gameover来判断。如果gameoverfalse,那说明游戏处于还没开始的状态。这时候通过判断dist的值来确定是否点击了开始按钮(注意:此时我们还没有绘制开始按钮),dist的值通过函数Math.sqrt(…)来计算,如果你看源码,你会发现我们直接使用的e.offsetX和e.offsetY来确定鼠标点击的位于canvas画布上的位置,这是因为我们将canvas画布的宽高都设置为与window的宽高一样,所以可以直接这么写。

如果gameovertrue表示游戏已经结束,这时候之前canvas画布上的监听事件都需要移除。点击重新开始后gameover变为falsecavnas画布重新添加监听函数,所有的子弹,陨石等需要清空。

action函数作为canvas画布的事件监听函数,具体可以查看代码。

//飞机转动角度
function move(e){
    player.deg = Math.atan2(e.offsetX - (W/2), -(e.offsetY  - (H/2)));
}

飞机跟随鼠标转动的函数如上,通过Math.atan2()来计算角度,它的调用在action()函数中,需要判断当前游戏所处的状态动态的添加。

7,子弹发射

通过action函数,我们完成了游戏与用户的交互。当飞机可以跟随鼠标进行旋转后,下一步就是点击鼠标可以发射子弹。子弹的各项参数我们在游戏开始后,通过点击鼠标,push进了bullets数组中。这时候我们只需要拿出来用就可以了。

注意看代码,我们发现在fire函数中,我们定义了一个变量distance,用于与陨石做碰撞检测。子弹绘制的部分你看看代码就懂了。重点是在计算子弹在运动中的实际位置,陨石与子弹间的碰撞就是基于陨石与子弹之间的距离。

我们先不管fire()函数中的碰撞检测部分。此时点击鼠标应该是可以发射子弹的了,那fire()函数应该在哪调用呢?我们把它放在_player()函数中,当满足一定的条件时就执行fire()函数。

这样基本上当我们再点击鼠标时你就会发现,子弹从飞机里发射出去。

8.陨石

陨石这一块可以分为两个部分:1.陨石的创建 2.陨石的运动。
在图中,陨石是有大有小,并且是从各个角度向中间运动。但是我们的雪碧图只提供了一种陨石图片,所以它们的大小还有位置我们通过随机数来获取。然后,将其放进asteroids数组中。这样就完成了陨石的初始化。

陨石运动的部分在_asteroids()函数中,这个函数中也定义了一个distance变量它的作用是判断陨石距离星球的距离。当distance小于某个值时,游戏结束。

另外,为了维持陨石的在游戏中的数目,我们这楼里加上了一个判定条件。

if(asteroids.length - destroyed < 10 + (Math.floor(destroyed/6))){
                newAsteroid();
            }

这样只要有陨石被击碎,就会重新生成一个陨石。这里我们也要获取陨石在游戏中的实际坐标,为的是判断游戏石佛结束。查看代码你会发现,我们将子弹与陨石之间的碰撞放在了fire()函数, 将陨石与星球的碰撞(即判断游戏是否结束)放到了_asteroids()函数中。当然你也可以把两个检测全部放在_asteroids()中,不过并不建议这么做。

9.子弹与陨石之间的碰撞检测

现在,当你开始游戏,点击鼠标。你会发现子弹从飞机发射,陨石从四面八方飞过来。但是两者相遇的时候并没有发生碰撞。回到fire()函数中,我们为子弹与陨石做基于距离的碰撞检测:

function fire(){
...
//碰撞检测
for(var j=0; j<asteroids.length; j++){
    if(!asteroids[j].destroyed){
         distance = Math.sqrt(Math.pow(
             asteroids[j].realX - bullets[i].realX, 2) +
             Math.pow(asteroids[j].realY - bullets[i].realY, 2));
    };
if(distance < (((asteroids[j].width/asteroids[j].size) / 2) - 4) + ((19 / 2) - 4)){
         destroyed += 1;
         asteroids[j].destroyed = true; //陨石消失
         bullets[i].destroyed   = true; //子弹消失
         explosions.push(asteroids[j]); //绘制爆炸图片
         expMusic.play(); //播放爆炸声
}}


}

与我们前面讲的一样,先通过两者的坐标计算距离,然后通过距离判断是否发生碰撞。如果发生碰撞就如注释中的一样,陨石消失,子弹消失,绘制爆炸图片,播放爆炸声。

10.爆炸图片绘制

爆炸图是一幅雪碧图,我们需要在子弹与陨石发生碰撞的时候对其经行绘制。具体看函数explosion(asteroid),之所以传入陨石的作为参数,是因为我们要确定绘制爆炸图片的位置。

11.陨石雨星球的碰撞

陨石与相求的碰撞,就是判断游戏是否结束。我们把它放在了_asteroid()函数中:

。。。

 distance = Math.sqrt(Math.pow(asteroids[i].realX -  W/2, 2) + Math.pow(asteroids[i].realY - H/2, 2));
 if (distance < (((asteroids[i].width/asteroids[i].size) / 2) - 4) + 100) {

      gameoverMusic.play();//音频播放
      gameover = true; //游戏结束
      playing  = false; 
      canvas.addEventListener('mousemove', action);
 }

 。。。

一样是基于距离的碰撞检测

12.启动函数

start()函数用于初始化星球,飞船等。然后,通过变量palying来绘制游戏的分数记录等,这些都是很简单的东西,你只需要看看代码就懂了,这里我就不多做介绍了。

13.添加音效

media文件中我放了两个音频文件,一个是爆炸音效,一个是游戏结束音效。他们都在碰撞检测的时候播放。唯一的问题是当鼠标点击的过快时,由于音频的播放需要在这段完成后再播放,所以你就会发现有的爆炸播放了声音,有的没有播放声音。在代码中我采取了一个简单的方案,减轻了这个效果,但是并没有完全消除。

expMusic.playbackRate = 2.0; //音频以2倍速度播放

14.总结

基本上到这游戏流程就讲完了,其中还有很多的细节需要自己去体会。当然,如果想写游戏,市场上有很多成熟的游戏引擎,比如:白鹭phaser.js等等都是不错的选择。本文主要的目的还是让你对我们讲到的碰撞检测,有一个深入的认识。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值