上班摸鱼除了刷掘金还能干啥? 快来跟我写个贪吃蛇吧

687 阅读5分钟

大家好,又是没活干的一天,今天用vue3和konva写一个贪吃蛇的游戏

先看一个效果

屏幕录制 2023-08-10 103458.gif

功能列表/玩法

  • 开始游戏和重新开始:初始块(初始为几块)
  • 蛇的移动:根据上下左右键盘四个方向移动,每次移动占据新的位置,释放以前的位置
  • 食物生成:开始的时候会在画布随机生成一个食物,蛇吃到后会在另一个位置重新生成
  • 蛇的成长:当吃到食物后蛇会加长一节
  • 碰撞检测:蛇撞到边界后游戏结束
  • 计分:每吃到一个食物就加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>

效果如下:

image.png

样式介绍

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)