大家好,又是没活干的一天,今天用vue3和konva写一个贪吃蛇的游戏
先看一个效果
功能列表/玩法
- 开始游戏和重新开始:初始块(初始为几块)
- 蛇的移动:根据上下左右键盘四个方向移动,每次移动占据新的位置,释放以前的位置
- 食物生成:开始的时候会在画布随机生成一个食物,蛇吃到后会在另一个位置重新生成
- 蛇的成长:当吃到食物后蛇会加长一节
- 碰撞检测:蛇撞到边界后游戏结束
- 计分:每吃到一个食物就加1
- 难易度:根据三个难度,加快蛇移动的速度
模板与样式
<template>
<p>分数: {{ score }}</p>
<div id="container" ref="stageContainer"></div>
<div class="btn-group">
<button @click="startGame">开始游戏</button>
<button @click="stopGame">停止游戏</button>
<button @click="restartGame">重新开始</button>
</div>
<div>
<label for="difficulty">难度:</label>
<select id="difficulty" v-model="difficulty">
<option value="1">简单</option>
<option value="2">中等</option>
<option value="3">困难</option>
</select>
</div>
</template>
<style>
#container {
width: 500px;
height: 500px;
background-color: #f0f0f0;
margin: 0 auto;
background-size: 20px 20px;
background-image:
linear-gradient(to right, grey 1px, transparent 1px),
linear-gradient(to bottom, grey 1px, transparent 1px);
}
.btn-group {
margin-top: 15px;
}
</style>
效果如下:
样式介绍
background-size: 20px 20px
: 这个属性设置了背景图片的尺寸大小,宽高为20px
background-image
:这个属性使用了两个 linear-gradient
函数来创建背景图像
第一个 linear-gradient
函数用于创建一个水平方向的线性渐变,从左到右。它的参数是 to right
,表示渐变的方向是从左到右,渐变的起始颜色是灰色 (grey
),终止颜色是透明 (transparent
),渐变的宽度是 1 像素。因此,这个渐变效果会在水平方向上创建一条细细的灰色线,然后后面是透明的
第二个 linear-gradient
函数用于创建一个垂直方向的线性渐变,从上到下。它的参数是 to bottom
,表示渐变的方向是从上到下,渐变的起始颜色是灰色 (grey
),终止颜色是透明 (transparent
),渐变的高度是 1 像素。因此,这个渐变效果会在垂直方向上创建一条细细的灰色线,然后后面是透明的
通过将这两个线性渐变的背景图像叠加在一起,就可以实现一个背景上有网格线的效果,每个渐变都只有 1 像素宽度或高度,所以它们会形成一个细小的网格,然后网格的大小是 20 像素乘以 20 像素。
游戏开发步骤
首先创建舞台和图层
const stageContainer = ref(null);
stage = new Konva.Stage({
container: stageContainer.value,
width: 500,
height: 500,
});
layer = new Konva.Layer();
stage.add(layer);
初始化蛇和食物的状态
然后body是数组,表示初始化蛇分为三块,xy是起始位置,食物位置随机
let stage: { destroy: () => void; add: (arg0: any) => void; },
food: SnakeBody
snake = {
body: [{ x: 2, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 0 }],
direction: 'right',
};
food = { x: Math.floor(Math.random() * 25), y: Math.floor(Math.random() * 25) };
绘制蛇的身体
snake.body.forEach((dot: { x: number; y: number; }) => { ... });
:这是一个forEach
循环,遍历了snake.body
数组中的每个点。对于数组中的每个点,我们执行一个回调函数。const rect = new Konva.Rect({ ... });
:在回调函数中,我们创建一个 Konva 库中的矩形对象rect
。x: dot.x * 20,
和y: dot.y * 20,
:通过将点的x
坐标和y
坐标乘以 20,得到矩形的实际位置,因为每个点的位置是以格子为单位的,每个格子的大小设为 20x20。width: 20,
和height: 20,
:指定矩形的宽度和高度,都设为 20,与格子的大小一致。fill: 'green',
:指定矩形的填充颜色为绿色,表示贪吃蛇的身体。layer.add(rect);
:将矩形对象添加到 Konva 的图层中,以便后续可以将其显示在画布上。
snake.body.forEach((dot: { x: number; y: number; }) => {
const rect = new Konva.Rect({
x: dot.x * 20,
y: dot.y * 20,
width: 20,
height: 20,
fill: 'green',
});
layer.add(rect);
});
绘制食物
const foodRect = new Konva.Rect({
x: food.x * 20,
y: food.y * 20,
width: 20,
height: 20,
fill: 'red',
});
layer.add(foodRect);
蛇的移动
我们需要写一个方法来更新蛇的位置
setInterval(() => {
updateGame();
}, 1000 / 60);
首先使用 Object.assign() 来创建副本是为了避免直接修改原始的蛇头位置,然后snake.direction是蛇移动的方向,然后通过方向判断,如果蛇向上移动('up'
),则将 head.y
减去 1,表示向上移动一格,如果向下移('down'
),则将 head.y
加1,表示向下移一格,左右也一样,head.x
减1,表示向左移一格,head.x
加1,表示向右移一格。
const head = Object.assign({}, snake.body[0]);
switch (snake.direction) {
case 'up':
head.y -= 1;
break;
case 'down':
head.y += 1;
break;
case 'left':
head.x -= 1;
break;
case 'right':
head.x += 1;
break;
}
过这个逻辑,我们可以根据蛇的移动方向来更新蛇头的位置
蛇的成长与新的食物
首先检查蛇头的位置是否和食物的位置相同,如果相同,表示蛇吃到了食物。然后,它会生成新的食物,当蛇吃到食物时,蛇的身体会增长,这是通过不移除蛇身的最后一部分来实现的。
如果蛇头的位置和食物的位置不相同,表示蛇没有吃到食物,那么就会移除蛇身的最后一部分,使蛇保持原来的长度
if (food.x === head.x && food.y === head.y) {
food = { x: Math.floor(Math.random() * 25), y: Math.floor(Math.random() * 25) };
score.value += 1;
} else {
snake.body.pop();
}
snake.body.pop();
:删除蛇身的最后一个点。这是为了保持贪吃蛇的长度不变,因为蛇没有吃到食物,所以蛇尾会向前移动一格,即删除最后一个点。
碰撞检测
head.x < 0
:如果蛇头的x
坐标小于 0,表示蛇头越过了左边界,发生了碰撞。head.y < 0
:如果蛇头的y
坐标小于 0,表示蛇头越过了上边界,发生了碰撞。head.x >= 25
:如果蛇头的x
坐标大于等于 25,表示蛇头越过了右边界,发生了碰撞。head.y >= 25
:如果蛇头的y
坐标大于等于 25,表示蛇头越过了下边界,发生了碰撞。
const checkCollision = (head: { x: number; y: number; }) => {
return (
head.x < 0 ||
head.y < 0 ||
head.x >= 25 ||
head.y >= 25 ||
snake.body.some((dot: { x: any; y: any; }) => dot.x === head.x && dot.y === head.y)
);
};
通过键盘上下左右控制
onMounted(() => {
window.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp':
if (snake.direction !== 'down') snake.direction = 'up';
e.preventDefault()
break;
case 'ArrowDown':
if (snake.direction !== 'up') snake.direction = 'down';
e.preventDefault()
break;
case 'ArrowLeft':
if (snake.direction !== 'right') snake.direction = 'left';
e.preventDefault()
break;
case 'ArrowRight':
if (snake.direction !== 'left') snake.direction = 'right';
e.preventDefault()
break;
}
});
})
这个应该没什么可说的,添加一个键盘事件监听器,监听用户的按键操作。当用户按下键盘时,根据按下的按键进行判断,使用 switch
语句来处理不同的按键情况。比如第一个分支,如果当前贪吃蛇的移动方向不是向下,则将贪吃蛇的移动方向设置为向上
第一次写,可能有点乱,源码地址:wangshaojie/moyu (github.com)