资讯专栏INFORMATION COLUMN

canvas小游戏——flappy bird

crossea / 1356人阅读

摘要:开始界面的定时器开始界面定时器定时器运行的次数定时器每运行次改变标题位置运行次数大家也可以理解为这就是开始界面,因为开始界面就是通过定时器一次次运行上面的函数所实现的。

前言

如果说学编程就是学逻辑的话,那锻炼逻辑能力的最好方法就莫过于写游戏了。最近看了一位大神的fly bird小游戏,感觉很有帮助。于是为了寻求进一步的提高,我花了两天时间自己写了一个canvas版本的。虽然看起来原理都差不多,但是实现方法大相径庭,如果有兴趣的话可以大家自己下载下来玩一玩,大概效果就像下面这样:


怎么样?是不是感觉难度巨大?...可能是因为我比较菜吧。相信高手还是大有人在的,随便过个几十关也是不在话下。但是如果有和我一样10关都过不了小菜鸡的话,根本不用丧气对吧?咱是程序员是不是?游戏不会玩,作弊还不会吗?咳咳,下面就是作弊的方法:

首先搞清楚结构

很简单,就是这样。

注意!我要开始说了 首先咱先加载一下所有的图片
// 图片集合
var imgs = {
  //创建图片
  bg: new Image(),
  grass: new Image(),
  title: new Image(),
  bird0: new Image(),
  bird1: new Image(),
  up_bird0: new Image(),
  up_bird1: new Image(),
  down_bird0: new Image(),
  down_bird1: new Image(),
  startBtn: new Image(),
  up_pipe: new Image(),
  up_mod: new Image(),
  down_pipe: new Image(),
  down_mod: new Image(),
  scroe0:new Image(),
  scroe1:new Image(),
  scroe2:new Image(),
  scroe3:new Image(),
  scroe4:new Image(),
  scroe5:new Image(),
  scroe6:new Image(),
  scroe7:new Image(),
  scroe8:new Image(),
  scroe9:new Image(),
  //加载图片
  loadImg: function (fn) {
    this.bg.src = "./img/bg.jpg";
    this.grass.src = "./img/banner.jpg";
    this.title.src = "./img/head.jpg";
    this.bird0.src = "./img/bird0.png";
    this.bird1.src = "./img/bird1.png";
    this.up_bird0.src = "./img/up_bird0.png";
    this.up_bird1.src = "./img/up_bird1.png";
    this.down_bird0.src = "./img/down_bird0.png";
    this.down_bird1.src = "./img/down_bird1.png";
    this.startBtn.src = "./img/start.jpg";
    this.up_pipe.src = "./img/up_pipe.png";
    this.up_mod.src = "./img/up_mod.png";
    this.down_pipe.src = "./img/down_pipe.png";
    this.down_mod.src = "./img/down_mod.png";
    this.scroe0.src = "./img/0.jpg";
    this.scroe1.src = "./img/1.jpg";
    this.scroe2.src = "./img/2.jpg";
    this.scroe3.src = "./img/3.jpg";
    this.scroe4.src = "./img/4.jpg";
    this.scroe5.src = "./img/5.jpg";
    this.scroe6.src = "./img/6.jpg";
    this.scroe7.src = "./img/7.jpg";
    this.scroe8.src = "./img/8.jpg";
    this.scroe9.src = "./img/9.jpg";
    var that = this;
    //添加定时器,判断图片是否加载完成
    var timer = setInterval(function() {
      if (that.bg.complete&&that.grass.complete
        &&that.title.complete&&that.startBtn.complete
        &&that.bird0.complete&&that.bird1.complete
        &&that.up_bird0.complete&&that.up_bird1.complete
        &&that.down_bird0.complete&&that.down_bird1.complete
        &&that.up_pipe.complete&&that.up_mod.complete
        &&that.down_mod.complete&&that.down_pipe.complete
        &&that.scroe0.complete&&that.scroe1.complete
        &&that.scroe2.complete&&that.scroe3.complete
        &&that.scroe4.complete&&that.scroe5.complete
        &&that.scroe6.complete&&that.scroe7.complete
        &&that.scroe8.complete&&that.scroe9.complete) {
        //删除定时器
        clearInterval(timer);
        //图片全部加载完成后,运行此函数
        fn();
      }
    }, 50)
  }
}

...抱歉有点长,但是怕破坏代码的结构,就全部拷下来了,上面的朋友快点下来吧,都是重复的没啥好看的。我来给大家解释一下,首先这是一个对象字面量,创建的时候新建了若干个图片对象,然后它有一个函数loadImg,只要一执行,就会给所有的图片添加路径,然后添加一个定时器每一段时间通过查询所有图片的complete属性判断图片是否全部加载完成。如果是,就删除这个定时器,并执行一段回调函数,还是很好理解的吧:),不过我感觉这种方法可能有点蠢,不知道各位高人有没有更好的方法?

接下来,就要开始画了

大家都知道,其实canvas就是画图,如果要用canvas实现动画效果的话,就只能一遍一遍的擦了画、画了擦了。

首先

先把几个固定不动的部分的绘制方法和清空画布的方法写在函数里

//绘制背景
  function drawBg() {
    ctx.drawImage(imgs.bg,0,0);
  }
  //绘制开始按钮
  function drawStartBtn() {
    ctx.drawImage(imgs.startBtn,130,300);
  }
    //清空画布
  function clean() {
    ctx.clearRect(0,0,canvas.width,canvas.height);
  }
然后

把会动的部分也加上

var v = 0;//草坪滚动的增量
  //绘制草坪
  function drawGrass() {
    //每次运行横坐标向左移
    ctx.drawImage(imgs.grass,3*v--,423);
    ctx.drawImage(imgs.grass,337+3*v--,423);
    if(3*v < -343){
      v=0;
    }
  }

这样每次运行一次,草坪就会向左移一点了

var shake = true;//标题的抖动状态
  //标题的抖动效果
  function titleShake() {
    if (shake) {
      ctx.drawImage(imgs.title,53,97);
      ctx.drawImage(imgs.bird1,250,137);
    }else{
      ctx.drawImage(imgs.title,53,103);
      ctx.drawImage(imgs.bird0,250,143);
    }
  }

这样通过改变shake的值,就可以使标题的抖动了。
机智的各位应该已经发现了,上面两个函数需要重复调用,才能产生动画的效果,所以这就是我接下来要讲的。

开始界面的定时器

var startTimer;//开始界面定时器
var startTime = 0;//定时器运行的次数
function startLayer() {
    startTimer = setInterval(function () {
      clean();
      drawBg();
      drawStartBtn();
      drawGrass();
      titleShake();
      //定时器每运行7次改变标题位置
      if(startTime == 7){
        shake = !shake;
        startTime = 0;
      }
      //运行次数+1
      startTime++;
      //window.requestAnimationFrame(startLayer)
    }, 24);
  }

大家也可以理解为这就是开始界面,因为开始界面就是通过定时器一次次运行上面的函数所实现的。然而上面定义的startTimer和startTime又有什么用呢,当然不是多此一举,首先,把这个定时器赋给一个变量,是为了在开始游戏的时候把这个界面关掉,也就是把这个定时器取消,往后看大家就明白了:)其次,startTime是为了记录定时器运行的次数,因为这个定时器刷新的实现极快,只有短短的24毫秒,如果标题以这个速度抖动的话,大家的眼睛一定受不了了吧,所以我设法让他慢下来,每运行7次抖动一次,当然大家可以设置9、10、11使它的频率更加缓慢(大家还可以尝试使用requestAnimation-
-Frame,那样性能更佳,但是控制频率略显麻烦。这里使用setInterval更容易理解)当然这个作弊没有半毛钱关系,不过下面就是重头戏了。

主角登场!!!
var bird = {
  bird: [imgs.bird0,imgs.bird1],//正常状态,图片
  up_bird: [imgs.up_bird0,imgs.up_bird1],//向上飞状态
  down_bird: [imgs.down_bird0,imgs.down_bird1],//向下掉状态
  posX: 100,//横坐标
  posY: 200,//纵坐标Y
  speed: 0,//速度
  index: 0,//翅膀挥动,切换图片的标
  alive: true,//存活状态
  //绘制小鸟
  draw: function (bird) {
    ctx.drawImage(bird,this.posX,this.posY);
  },
  //飞行中
  fly: function () {
    //纵坐标随速度改变
    this.posY+=this.speed;
    //加速度为1
    this.speed++;
    //如果坠地,死亡
    if(this.posY >= 395){
      this.speed = 0;
      this.draw(this.bird[this.index]);
      this.dead();
    }
    //如果撞顶,弹回来
    if(this.posY <= 0){
      this.speed = 6;
    }
    //如果速度为正,则向下,反之,则向上,否则水平
    if(this.speed>0){
      this.draw(this.down_bird[this.index]);
    }else if(this.speed<0){
      this.draw(this.up_bird[this.index]);
    }else{
      this.draw(this.bird[this.index]);
    }
    //确保坠落速度不会太快
    if(bird.speed > 6){
      bird.speed = 6;
    }
  },
  //煽动翅膀,切换图片
  wingWave: function () {
    this.index++;
    if(this.index > 1){
      this.index = 0;
    }
  },
  //死亡
  dead: function() {
    this.alive = false;
  }
}

...当然这只是主角的代码,一个对象字面量。但是它可以操控主角的所有行为(虽然也没有几个行为...),首先就是画出主角draw(),通过传进不同的图片绘制出主角不同情况下的英姿...然后是wingWave(),通过改变index,切换上面定义的图片数组中的图片,也就是挥翅膀。再然后就是飞行fly(),在飞行过程中主角会碰到各种各样的事故,像是飞的太高撞到天花板啊,或是飞的太低,摔了个狗啃屎。再干脆点一头撞死在了钢管上,但是这个函数并不在这里,因为小鸟撞死在钢管上到底是小鸟的行为,还是钢管的行为呢,我还没想明白,所以干脆放在了全局中。

  //判断是否碰撞
  function isHit(oPipe){
    if(bird.posX+bird.bird[0].width>oPipe.posX&&bird.posXoPipe.down_posY){
        bird.dead();
      }
    }
  }

就像这样,通过判断小鸟和钢管的位置判断小鸟是不是撞在钢管上了。反正结果还是撞死bird.dead()。看到这里相信不用我说,大家也明白了吧,只要将这段代码注释掉,我们的小鸟不就练成的绝世铁头功,钢管都捅穿给你看。或者稍稍增大一点小鸟会被碰撞到的体积,那就是凌波微步、轻功管上飘了呀。说了半天,还没告诉大家这个水管又是哪里来的。

钢管
//水管类
class Pipe {
  constructor(up_pipe,up_mod,down_pipe,down_mod) {
    //构造函数
    this.up_pipe = up_pipe;//上水管头部
    this.up_mod = up_mod;//上水管中间部分
    this.down_pipe = down_pipe;
    this.down_mod = down_mod;
    this.up_height = Math.floor(Math.random()*60);//随机生成上管体高度
    this.down_height = (60 - this.up_height)*3;//保证所有上下水管距离相同
    this.posX = 300;//横坐标
    this.up_posY = this.up_height*3+this.up_pipe.height;//上水管纵坐标
    this.down_posY = 362-this.down_height;//下水管纵坐标
    this.hadSkipped = false;//是否被越过
    this.hadSkippedChange = false;//去重
  }
  //绘制水管
  drawPipe() {
    ctx.drawImage(this.up_pipe,this.posX,this.up_height*3);
    ctx.drawImage(this.down_pipe,this.posX,362-this.down_height);
  }
  //绘制管体
  drawMods() {
    for(var i=0;i



又是一段冗长的代码,大家不要急躁,我来给大家详细解释,水管分为两部分,一部分是固定的管口,还有一部分是为了控制钢管长度的管体,在上面的图片也可以看到,每一关的管道是分为上下两个的——up_pipe和down_pipe,也就是说我们看到的钢管是由数个相同的管体加管口构成的,这里管体的数量是随机的,这样就可以使管道拥有随机的长度了。然后为了保证上下两个钢管的中间距离固定,下管道的高度就是总高度减去上管道的高度,嗯,这里需要理一理,大家也可以直接去看我的代码。有了上面的理论,接下来就简单了,绘制管口drawPipe(),注意给管体预留出位置来,再绘制管体drawMods(),用一个for循环依次绘制出数个管体叠加在一起的样子。水管移动move(),就是改变水管的横坐标了。这里可以通过改变上下水管高度的总值,来增加上下水管之间的距离,是不是游戏难度一下就降了很多?再有就是判断水管是否被小鸟跨越的hadskiped属性,往下看

//判断是否越过水管
  function isSkipped(oPipe) {
    if(bird.posX>oPipe.posX+oPipe.down_pipe.width){
      //水管已经被越过
      oPipe.hadSkipped = true;
      //确保水管只被越过一次
      if(!oPipe.hadSkippedChange&&oPipe.hadSkipped){
        //分数+1
        scroll++;
        oPipe.hadSkippedChange = true;
      }
    }
  }

我是通过判断水管的位置是否已经位于小鸟的后面来判断,小鸟是否越过了水管的,如果越过了就+1分,至于没越过就是通过前面讲过到的isHit()判断了,因为不是同一时间段发生的事情所以不能放在一起。

计分表

var scroll = 0;//当前得分
var scrollImg = [imgs.scroe0,imgs.scroe1,imgs.scroe2,
              imgs.scroe3,imgs.scroe4,imgs.scroe5,
              imgs.scroe6,imgs.scroe7,imgs.scroe8,
              imgs.scroe9];//存储数字图片
  //绘制当前得分
  function drawScore() {
    //每绘制一位数,向右移23,绘制下一位数
    for(var i=0;i

首先,把所有分数有关的图片放到这里scrollImg来,方便使用。然后判断数字的位数,也就是个十百千万。循环并截取每个位数,再通过相应的图片绘制出来,并且每绘制一个位数的图片位置向右移23,这样数字就不会叠在一起了。这里有一种最没意思的作弊方法,就是手动调整分数,但这只是一个数字,游戏的乐趣果然还是在于过程,下面...

游戏开始!
//游戏界面
  function gameLayer() {
    gameTimer = setInterval(function () {
      clean();
      drawBg();
      drawGrass();
      if(gameTime%5 == 0){
        if(gameTime == 30){
          createPipes();
          gameTime = 0;
        }
        bird.wingWave();
      }
      gameTime++;
      for(var i = 0;i< pipes.length;i++){
        pipes[i].move();
        isHit(pipes[i]);
        isSkipped(pipes[i]);
      }
      drawScore();
      bird.fly();
      //如果小鸟死了
      if(!bird.alive){
        gameOver();//游戏结束
        reset();//数据重置
      }
    }, 24);
  }

...看到这里,估计已经有人在骂我了,讲了半天游戏还没开始...好吧,你们看,其实游戏的界面也不过是一个定时器,将前面讲到的函数和代码,无脑的、重复的执行着。然后这里一定要注意画图的顺序,不然后画的部分会把前面覆盖掉,其次这里的gameTimer和gameTime也和开始界面中startTimer、startTime起到类似的作用,每过一段较长的时间生成一个水管,也就是通过水管类实例化一个水管对象,具体的方法被我封装进一个createPipes函数里了。

var pipes = [];//用于存放水管
function createPipes() {
    var pipe = new Pipe(imgs.up_pipe,imgs.up_mod,imgs.down_pipe,imgs.down_mod);
    //添加进pipes中,如果已经有三个水管,则依次替换
    if(pipes.length<3){
      pipes.push(pipe);
    }else{
      pipes[index] = pipe;
      index++;
      if(index >= 3){
        index = 0;
      }
    }
  }

因为实现的方法没有想象中那么简单,首先我们要创造一个水管的数组,它的作用就是为了控制水管的数量,不然我们的定时器就会一遍一遍的创造出无数的水管,但是前面的水管早就离我们远去,所以我就用数组把水管装起来,控制只有一个屏幕的水管,也就是三个。如果创建了超过三个水管,就会把最前面一个替换掉,因为它已经超出了我们的视野。

响应事件

光有动画也不行,只能看不能玩有个皮用啊。所以我们当然要添加响应事件了。

//键盘点击事件
  function kd(e) {
    if (e.keyCode === 32) {
      bird.speed = -10;
    }
  }
  //触屏事件
  function ts() {
    bird.speed = -10;
  }
  //start按钮点击事件
  function startBtn_click(e) {
    //判断点击位置
    if(e.clientX>canvas.offsetLeft+canvas.width/2-imgs.startBtn.width/2
      &&e.clientXcanvas.offsetTop+300){
      clean();
      //清除开始界面定时器
      clearInterval(startTimer);
      gameLayer();
      //添加响应事件
      window.addEventListener("keydown",kd,false)
      window.addEventListener("touchstart",ts,false)
      //删除start按钮响应事件
      canvas.removeEventListener("click",startBtn_click,false);
    }
  }
  canvas.addEventListener("click", startBtn_click , false);

这就是所有的响应事件了,通过按空格键和点击屏幕都可以改变小鸟的速度,只要把这个速度调整到一个比较舒服的程度,游戏难度就会大大降低。其次,因为canvas是一个整体,所以我们没有办法直接监听里面图片按钮的响应事件,只能退而求其次,判断点击的位置是否在按钮的位置上了,就上面那段有点长的if判断语句。

游戏结束

假如我们的主角真的一个不小心如我们所料的撞死在了钢管上(往上翻,就在游戏开始那里),那就表示gameOver();

  //游戏结束
  function gameOver(){
    //清除定时器
    clearInterval(gameTimer);
    //清除窗口响应事件
    window.removeEventListener("keydown",kd,false);
    window.removeEventListener("touchstart",ts,false);
    //绘制GAME OVER
    ctx.font = "50px blod";
    ctx.fontWeight = "1000"
    ctx.fillStyle = "white";
    ctx.fillText("GAME OVER", 20, 200);
    drawStartBtn();
  }



整个世界都平静了下来,定时器关掉,响应事件移除掉,然后绘上大大的、惨白的GAME OVER,下面附带一个游戏开始时就出现的start按钮。不是有一句话说的是,结束不过是新的开始吗,你又可以再来一局了。......好吧,这个就是我为了偷懒随便搞搞的。不过这还没完,数据还得重置一下,不然怎么重新开始。

  //重置数据
  function reset(){
    bird.posY = 200;
    bird.speed = 0;
    bird.alive = true;
    pipes = [];
    scroll = 0;
    canvas.addEventListener("click", startBtn_click , false);
  }

最后再给这个start按钮添加上点击事件,大功告成!这就是我调整难度之后的样子:


啧啧啧,这种闲庭信步的感觉......

果然游戏还是有点难度才有意思......

总结

吁...一篇又臭又长、废话又多的文章终于写完了,如果大家觉得有帮助,或者对这篇文章有兴趣的话,就赏个赞。如果觉得我的程序有问题,或者有别的想说的,都可以在评论里告诉我,我会看的。

我的项目地址:https://github.com/tzc123/can...

参考项目地址:http://www.jianshu.com/p/45d9...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84729.html

相关文章

  • PaddlePaddle版Flappy-Bird—使用DQN算法实现游戏智能

    摘要:刚刚举行的深度学习开发者峰会上,发布了版本,这一版新增了等一系列并行算法。专注于游戏智能少儿趣味编程两大领域。有了贝尔曼最优方程,我们就可以通过纯粹贪心的策略来确定,即仅仅把最优动作的概率设置为,其他所有非最优动作的概率都设置为。 刚刚举行的 WAVE SUMMIT 2019 深度学习开发者峰会上,PaddlePaddle 发布了 PARL 1.1 版本,这一版新增了 IMPALA、A...

    vpants 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<