小游戏管理平台-飞翔的小鸟

194 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第四天,点击查看活动详情

线上地址:mygame.codeape.site

源码:gitee.com/wooden-join…

开发技术

vue2 + js + canvas

需求

小鸟往前飞,会一直往下掉,点击游戏区域,小鸟抬一下头,需要避让管道,当小鸟触碰到天空、地板或管道游戏结束。

代码讲解

初始化数据

初始化,获取 canvas 2d上下文,根据画板宽度生成对应数量的管道,并根据天空到地板的距离 / 2 再与一个随机数进行搭配,计算出每根管道应该伸出的长度。然后还需对图片进行加载处理,因为画到 canvas 上的图片都需要先进行加载的。紧接着就可以开始游戏,对数据进行绘画了。

init() {
  setTimeout(() => {
    this.canvas = this.$refs.canvas;
    this.ctx = this.canvas.getContext("2d");
    // 根据地图宽度 生成对应数量的管  (750 + 52 ) / 4 = 200.5
    for (
      let n = 401;
      n <= this.canvas.width + 200.5 + 52;
      n += this.pipe.gapX
    ) {
      // r: 管突出的距离
      this.allPipe.push({
        x: n,
        // 随机数为:天空到地面的距离 / 2 (限制最小为 54) 500-112=388
        r: parseInt(Math.random() * 140 + 54),
      });
    }
    this.onKeyDown()
    // 加载图片
    let that = this;
    this.loadImage(this.imglist, function (imgObj) {
      that.imgOnloadObj = imgObj
      that.startAnimation()
    });
  }, 300);
},

加载图片

创建 Image 实例,并将图片加载进去,当所有图片都 onload 加载完成后,就可以开始绘画了。

loadImage(imgUrl, callback) {
  var imgObj = {}; // 保存图片资源
  var tempImg,
    imgLength = 0,
    loaded = 0;
  for (var key in imgUrl) {
    imgLength++; // 初始化要加载图片的总数
    tempImg = new Image();
    tempImg.src = imgUrl[key];
    imgObj[key] = tempImg;
    tempImg.onload = function () {
      loaded++; // 统计已经加载完毕的图像
      // 所有的图片都加载完毕
      if (loaded >= imgLength) {
        // 把加载完毕的资源传给回调供其使用
        callback(imgObj);
      }
    };
  }
},

开始绘画

我这里没有继续使用定时器了,因为后来我知道了 requestAnimationFrame 这个方法,它能比定时器更高效的刷新。据文档描述时间间隔能达到每秒 60 帧。搭配递归调用 run 函数,即可持续绘画。

startAnimation() {
  let that = this;
  (function go() {
    that.run();
    if (that.isPause || that.gameOver) {
      return
    }
    // 时间间隔为 1000/60 每秒 60 帧
    requestAnimationFrame(go);
  })();
},

run 函数处理数据并绘画

每一次的都需要清空画板,然后更新天,管道,地和鸟的数据,然后再重新对他们进行绘画。

run() {
  const imgObj = this.imgOnloadObj
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  // 天
  this.updateSky();
  this.drawSky(imgObj["sky"]);
  // 管
  this.updatePipe();
  this.drawPipe(imgObj["pipe_t"], imgObj["pipe_b"]);
  // 地
  this.updateLand();
  this.drawLand(imgObj["land"]);
  // 鸟
  this.updateBird();
  this.drawBird(imgObj["bird"]);
},

绘画天空、管道和地板的注意点

由于飞翔的小鸟是需要往左侧持续移动的,所以在绘画时,以上图片的宽度,都是需要大于画板的两倍的,这样才能确保持续移动的时候看上去天空、管道和地板是无限的。(管道宽度是大于当前画板中的宽度再加一个即可,意思和天空地板差不多。)

绘画天空

需要根据当前的速度来控制天空的x1,x2坐标,当有一部分天空移动到左侧超越一倍 canvas 的宽度时(即这一部分天空不可见后),需要将该天空移动到 canvas 右侧位置,这样能有无限轮播的效果。

updateSky() {
  this.sky.x -= this.xSpeed;
  this.sky.x2 -= this.xSpeed;
  this.sky.x <= -750 ? (this.sky.x = 750) : "";
  this.sky.x2 <= -750 ? (this.sky.x2 = 750) : "";
},

根据数据在 canvas 中绘画天空

drawSky(img) {
  this.ctx.drawImage(img, this.sky.x, 0);
  this.ctx.drawImage(img, this.sky.x2, 0);
},

绘画地

地的处理和天空的处理是一模一样的。因为当时找到的素材天空和地板是分开的,其实这里可以把这两幅图合起来,当成一块背景来处理。但是数据还是要有两份的,因为天空和地板对小鸟的影响是区分出来的

updateLand() {
  this.land.x -= this.xSpeed;
  this.land.x2 -= this.xSpeed;
  this.land.x <= -750 ? (this.land.x = 750) : "";
  this.land.x2 <= -750 ? (this.land.x2 = 750) : "";
},
drawLand(img) {
  this.ctx.drawImage(img, this.land.x, this.canvas.height - this.land.y); // x 和 y 坐标
  this.ctx.drawImage(img, this.land.x2, this.canvas.height - this.land.y);
},

绘画管道

由于一开始初始化数据时,已经对管道处理好了,所以只需移动管道的x坐标即可,然后就是对其进行绘画。

updatePipe() {
  for (let p of this.allPipe) {
    p.x -= this.xSpeed;
  }
},

绘画完管道后,当管道移动到 canvas 画板左侧完全不可见后,将其位置重新放到画板最右边不可以区域,这样就是一个轮回了。

drawPipe(imgTop, imgBottom) {
  for (let p of this.allPipe) {
    // 管突出的距离   420是管的长度
    this.ctx.drawImage(imgTop, p.x, p.r - 420);
    this.ctx.drawImage(imgBottom, p.x, p.r + this.pipe.gapY);
    // 52 管的宽度
    if (p.x <= -52) {
      p.x = 750;
      p.r = parseInt(Math.random() * 120 + 74);
    }
  }
},

绘画小鸟

更新鸟的y坐标,让它下落,并持续加快它的下落速度。这样就有一种掉落的感觉了。

updateBird() {
  this.bird.y += this.bird.speed;
  // 加快下落速度
  this.bird.speed += this.bird.addSpeed;
},

绘画小鸟,需要对小鸟进行旋转,让鸟的头部往下,这样才能形成掉落感。

drawBird(img) {
  let b = this.bird;
  this.ctx.save(); // 保存当前的绘图状态
  this.ctx.translate(b.x, b.y);
  // 弧长 = nπr / 180,n是角度数
  this.ctx.rotate((Math.PI / 180) * 10 * b.speed);
  // (image, x, y, 切片width, 切片height, 切片x, y, w, h)
  this.ctx.drawImage(img, 0, 0, b.w, b.h, -b.w / 2, -b.h / 2, b.w, b.h);
  this.ctx.restore(); // 恢复之前保存的绘图状态
},

点击画板,使小鸟上升

让小鸟的下落速度变为一个负值,其实就是让小鸟抬头上升,每点击一次就上升一点,由于绘画时,默认该值会往正数增加,所以就能形成小鸟锤头下降又抬头上升的感觉了。

birdUp() {
  if(this.isPause) return
  this.bird.speed = this.clickUpNum;
},

watch 监听小鸟

判断小鸟有没有和天空、地板或管道发生碰撞,发生碰撞则游戏结束,根据小鸟的y坐标即可判断是否和天空和地板发生碰撞。遍历管道,当小鸟的x坐标在某个管道之中时,再判断小鸟的y坐标是否在管道中即可判断是否发生碰撞。

bird: {
  handler(b) {
    // 撞到地或天空了
    if (b.y <= b.h / 3 || b.y >= this.canvas.height - this.land.y - b.h / 2) {
      this.gameOver = true;
    }
    // 撞到管道了
    for (let p of this.allPipe) {
      if (
        b.x >= p.x &&
        b.x <= p.x + 52 &&
        !(b.y > p.r && b.y < p.r + this.pipe.gapY)
      ) {
        this.gameOver = true;
      }
    }
  },
  deep: true,
},

感悟

飞翔的小鸟是我的第二个 canvas 小游戏了,我记得当初暑假在家在做小游戏管理平台,当时的我完成了贪吃蛇后,而这个游戏也没有想象中的复杂,只需区分好天空、地板、管道和小鸟4个对象即可,最后好像花了一天多两天不到的时间就完成了。而我对 canvas 来做游戏就更加有心得了。

小游戏管理平台:juejin.cn/post/714916…