开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第三天,点击查看活动详情
线上地址:mygame.codeape.site
开发技术
vue2 + js + canvas
需求
蛇会自动往前走,键盘上下左右控制方向,当蛇吃到蛋后,长度加一,并重新生成蛋,撞到墙或咬到自己游戏结束。附加功能:每隔10秒墙会自动打开/关闭,蛇可以在打开时穿越墙并传送到另一边,并为其穿梭途中增加一个传送圈。
代码讲解
初始化页面
获取 canvas 画板2d上下文,将画板根据自定义的 size 裁剪行列的方格;然后创建蛇和蛋的数据,再通过 canvas 绘画出来。
init() {
setTimeout(() => {
this.canvas = this.$refs.canvas;
// let canvas = document.getElementById("mycanvas");
this.ctx = this.canvas.getContext("2d");
// 定义行列
this.row = this.canvasWidth / this.size;
this.col = this.canvasHeight / this.size;
// 刚开始先画出蛇和蛋来
this.updateEgg();
this.drawEgg();
this.drawSnake();
// 监听键盘事件
this.onKeyDown();
if(!this.isPause) this.startGame();
}, 200);
},
蛋的绘制
首先是需要更新蛋的位置,由于该函数后面还需用到,所以需要判断蛋的出现位置是否在蛇身体里,如果在,则需要重新更改其位置。
updateEgg() {
// 当蛋出现在蛇身体里面的时候,重新执行
do {
this.egg.x =
(Math.floor(Math.random() * this.col - 1) + 1) * this.egg.size;
this.egg.y =
(Math.floor(Math.random() * this.row - 1) + 1) * this.egg.size;
} while (this.snake.body.includes({ x: this.egg.x, y: this.egg.y }));
},
紧接着就是将蛋绘画出来,填充颜色并根据蛋的位置绘画出代表蛋的方块。
drawEgg() {
this.ctx.fillStyle = "#adff2f";
this.ctx.fillRect(this.egg.x, this.egg.y, this.egg.size, this.egg.size);
},
蛇的绘制
遍历蛇身的对象数组,根据坐标将蛇绘制出来,每一段蛇身对应一个方块;当蛇移动时,还需保存好蛇穿越墙的坐标,用于后面绘制蛇穿越墙壁的光圈。
drawSnake() {
this.ctx.fillStyle = "#fff";
for (let i = 0; i < this.snake.body.length; i++) {
const { x, y } = this.snake.body[i];
this.ctx.fillRect(x, y, this.snake.size, this.snake.size);
}
this.ctx.fillRect(
this.snake.x,
this.snake.y,
this.snake.size,
this.snake.size
);
// 处理穿墙 并保存穿墙的位置
if (!this.isWall) {
const { width, height } = this.canvas;
if (this.snake.x >= width) {
this.throughWall.x = width - this.size;
this.throughWall.y = this.snake.y;
this.throughWall.isTurn = false;
this.snake.x = 0;
}
if (this.snake.x < 0) {
this.throughWall.x = 0;
this.throughWall.y = this.snake.y;
this.throughWall.isTurn = false;
this.snake.x = width;
}
if (this.snake.y >= height) {
this.throughWall.x = this.snake.x;
this.throughWall.y = height - this.size;
this.throughWall.isTurn = true;
this.snake.y = 0;
}
if (this.snake.y < 0) {
this.throughWall.x = this.snake.x;
this.throughWall.y = 0;
this.throughWall.isTurn = true;
this.snake.y = height;
}
}
},
开始游戏
创建计时器,然后不断的清空画板,然后绘制蛋,更新蛇的位置,画蛇(如果蛇吃到了蛋,就需要更新蛇的数据和对应蛋的数据),以此进行循环绘画。
// 开启游戏
startGame() {
this.gameTimer = setInterval(() => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawEgg();
this.updateSnake();
this.drawSnake();
if (this.eatEgg()) {
this.updateEgg();
}
}, this.drawTime);
// 开启墙的定时器
this.wallTimer = setInterval(() => {
this.isWall ? this.wallTime-- : this.wallTime++;
}, 1000);
},
蛇的移动
遍历蛇身,将蛇前面一节的位置给到后面那节即可,当蛇吃到了蛋,则需要把当前蛋的位置作为新的蛇头,即把该位置坐标给到蛇,然后让它来带领蛇身移动。
updateSnake() {
// 让蛇的后一节等于前一节的位置
for (let i = 0; i < this.snake.body.length - 1; i++) {
this.snake.body[i] = this.snake.body[i + 1];
}
// 吃到东西了, 保存蛇的身体位置,并且将其作为蛇头,不断带领蛇身移动
if (this.snake.length > 0) {
this.snake.body[this.snake.length - 1] = {
x: this.snake.x,
y: this.snake.y,
};
}
this.snake.x += this.snake.xSpeed;
this.snake.y += this.snake.ySpeed;
},
键盘对蛇的移动
使用 document.onkeydown 方法监听键盘事件,左右下,都是调用 move 函数。
- 注:this.$store.commit("setCurrentKey", "left"); 只是我用来设置项目中方向键的样式的。可以不用管。
// 监听用户的键盘事件
onKeyDown() {
document.onkeydown = (e) => {
switch (e.code) {
case "ArrowLeft":
// 判断不能反方向
if (this.direction == "right") break;
this.$store.commit("setCurrentKey", "left");
this.move(-this.size * 1, 0);
break;
case "ArrowUp":
if (this.direction == "down") break;
this.$store.commit("setCurrentKey", "up");
this.move(0, -this.size * 1);
break;
case "ArrowRight":
if (this.direction == "left") break;
this.$store.commit("setCurrentKey", "right");
this.move(this.size * 1, 0);
break;
case "ArrowDown":
if (this.direction == "up") break;
this.$store.commit("setCurrentKey", "down");
this.move(0, this.size * 1);
break;
case "Space":
this.pause();
break;
}
};
},
move 函数其实就是将蛇往前移动一格即可
move(x, y) {
(this.snake.xSpeed = x), (this.snake.ySpeed = y);
},
蛇吃到蛋
蛇吃蛋,其实就是判断蛇和蛋的坐标是否相等,相等就把蛇长度加一,增加得分就好
eatEgg() {
if (this.snake.x == this.egg.x && this.snake.y == this.egg.y) {
this.snake.length++;
this.score += 10;
return true;
}
return false;
},
watch 监听蛇的移动
首先需要判断当前蛇的移动方向,当蛇头坐标等于自身任何一个坐标,则代表蛇咬到了自己,则蛇死亡。而当蛇身还在穿墙,则需要控制光圈持续展示(代表当前在穿墙)。当墙存在时,如果蛇头触碰到了墙,则蛇死亡。
snake: {
handler(s) {
let isThrough = false;
// 判断当前的方向
if (
s.body.length > 0 &&
s.body[s.length - 1] &&
s.body[s.length - 1].x
) {
// 将跟着头部的那节身体坐标 解构出来
let { x, y } = s.body[s.length - 1];
if (s.y == y && s.x - x > 0) {
this.direction = "right";
} else if (s.x - x < 0) {
this.direction = "left";
} else if (s.x == x && s.y - y > 0) {
this.direction = "down";
} else if (s.y - y < 0) {
this.direction = "up";
}
}
for (let item of s.body) {
// 判断蛇咬到了自己
if (s.x == item.x && s.y == item.y) {
this.snakeDie();
}
// 只要有一个蛇身还在穿墙,就一直显示旋转的圈
if (item.x == this.throughWall.x && item.y == this.throughWall.y) {
this.isThroughWall = true;
isThrough = true;
}
}
if (!isThrough) {
this.isThroughWall = false;
}
// 判断蛇是否触碰了边界;
const { width, height } = this.canvas;
if (
this.isWall &&
(s.x < 0 || s.x >= width || s.y < 0 || s.y >= height)
) {
this.snakeDie();
}
},
deep: true,
},
收获
该贪吃蛇小游戏是我的一个 canvas 小游戏,一开始学习 canvas 学了大概一个星期吧。而且当时暑假在家想到了一个毕设:小游戏管理平台;(因为之前已经写过两个小游戏了,不想浪费掉)
然后就想着用 canvas 来试着来做个小游戏,后来就在b站找了个视频作为参考,视频中是 js 版本的,由于当时也刚学 vue,所以就改写成 vue2 的版本。整个小游戏下来,对 js 基本功提升了不少,同时对 canvas 的绘制更加熟悉了。了解到canvas 绘画比直接 js 操作 dom,在性能上好不少。
参考视频地址:www.bilibili.com/video/BV1NC…
小游戏管理平台:juejin.cn/post/714916…