开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第四天,点击查看活动详情
线上地址:mygame.codeape.site
开发技术
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…