基于Vue实现简单的贪食蛇游戏

作者:AntPro 时间:2024-04-27 16:13:17 

贪食蛇是一个非常经典的游戏, 在游戏中, 玩家操控一条细长的直线(俗称蛇或虫), 它会不停前进, 玩家只能操控蛇的头部朝向(上下左右), 一路拾起触碰到之物(或称作“豆”), 并要避免触碰到自身或者边界. 每次贪吃蛇吃掉一件食物, 它的身体便增长一些.

本项目使用的技术栈和标题一样非常的简单, 只有用到 vue, 主要实现使用的是 HTML + CSS 动画

代码实现可以参考: CodeSandbox

实现游戏棋盘

在游戏描述中有提到, 玩家操纵的蛇要避免触碰到自身或者边界. 这就需要我们实现一个有边界的游戏棋盘.

在 html 中, 我们可以使用 css 的 width、border 和 height 属性来实现一个简单的、具有边界的容器:

在 App.vue 中的实现(功能节选)

<template>
 <div class="game-box"></div>
</template>
<style>
body {
 display: flex;
 width: 100vw;
 height: 100vh;
 margin: 0;
}
.game-box {
 position: relative;
 width: 500px;
 height: 500px;
 border: 1px solid #ddd;
 margin: auto;
}
</style>

其中 position: relative; 是为了之后的 position: absolute 元素能够在游戏棋盘中的显示正确的位置.

实现蛇与豆的实体

展示豆的方式可以使用一个 div 元素, 使用 position: absolute 与 left、top 属性来实现豆的位置:

在 App.vue 中的实现(功能节选)

<template>
 <div class="game-box">
   <div class="snake-food" :style="{ top: foodPos.y + 'px', left: foodPos.x + 'px' }" />
 </div>
</template>
<script>
export default {
 data() {
   return {
     foodPos: {},
   };
 },
};
</script>
<style>
.snake-foot {
 position: absolute;
 /* 保证初始位置不可见 */
 top: -9999px;
 left: -9999px;
 width: 10px;
 height: 10px;
 /* 你也可以与众不同 */
 background-color: rgb(207, 38, 38);
 z-index: 2;
}
</style>

实现蛇就需要稍稍拆解一下需求. 我们知道蛇在吃了豆之后, 就会增长一些. 这看起来就像是一条单向的链表, 在蛇吃到豆之后便插入一条. 而且插入数据的部分只有在其尾部, 并不需要链表的便捷插入特性, 所以我们可以使用一个保存位置信息的数组来实现蛇的身体. 并且独立出蛇的头部来引导蛇的移动. 在这里我们保留了指向尾部的引用, 以便在蛇吃到豆之后, 可以快速的将新的蛇尾插入到最后:

在 App.vue 中的实现(功能节选)

<template>
 <div class="game-box">
   <div ref="snake" class="snake">
     <!-- 蛇的头部用来引导蛇的移动 -->
     <div :style="{ top: headerPos.y + 'px', left: headerPos.x + 'px' }" ref="snakeHeader" class="snake-header" />
     <!-- 蛇的身体, 使用连续的数组实现 -->
     <div
       :key="uuid"
       :uid="uuid"
       v-for="{ pos: { y, x }, uuid } in snakeBodyList"
       :style="{ top: y + 'px', left: x + 'px' }"
       class="snake-body"
     />
   </div>
 </div>
</template>
<script>
// 蛇身的大小单位
const defaultUnit = 10;

function updatePos(pos, direction) {
 // 规避引用
 const newPos = { ...pos };
 switch (direction) {
   case directionKeys.up:
     newPos.y -= defaultUnit;
     break;
   case directionKeys.down:
     newPos.y += defaultUnit;
     break;
   case directionKeys.left:
     newPos.x -= defaultUnit;
     break;
   case directionKeys.right:
     newPos.x += defaultUnit;
     break;
   default:
     throw new Error('NotFind');
 }
 return newPos;
}

export default {
 data() {
   return {
     // 蛇身自增的 uuid
     id: 0,
     // 蛇的头部位置
     headerPos: {},
     // 保存尾部的位置信息
     lastPos: {},
     // 保存蛇的身体位置信息
     snakeBodyList: [],
   };
 },
 methods: {
   init() {
     // 初始化数据
     const initData = { x: 250, y: 250 };
     this.direction = directionKeys.left;
     this.lastPos = { ...initData, direction: this.direction };
     this.headerPos = { ...initData };
     this.snakeBodyList = Array(defaultUnit).fill(0).map(this.createBody);
   },
   createBody() {
     const { x, y } = this.lastPos;
     // 判断是否属于同水平方向
     const isLower = this.direction === directionKeys.up || this.direction === directionKeys.left;
     const pos = {
       // 同水平方向刚好差 2 的数值, 40 - 38 = 2, 39 - 37 = 2
       ...updatePos({ x, y }, isLower ? this.direction + 2 : this.direction - 2),
     };
     // 保存尾部的位置信息
     this.lastPos = pos;
     return {
       uuid: this.id++,
       pos,
     };
   },
 },
};
</script>

当我们需要添加新的蛇身时, 只需要调用 createBody 方法, 并将其添加至蛇的身体数组尾部即可:

// 使用push方法添加蛇身至身体数组尾部
this.snakeBodyList.push(this.createBody());

实现蛇的移动方向(输入控制)

我们知道, 用户在键入一个按键时, 如果我们有监听 keydown 事件, 浏览器会触发回调函数并提供一个KeyboardEvent 对象. 当我们要使用键盘来控制蛇的移动方向时, 就可以使用该事件对象的 keyCode 属性来获取键盘按键的编码.

其中 keyCode 属性的值可以参考 键盘编码.

实现这个功能我们可以在全局对象 window 上添加一个 keydown 事件监听函数, 并将键盘按键的编码保存在实例中, 考虑到用户可能会输入多个键盘按键, 所以我们需要检查是否为方向键, 并且跳过同一个水平方向上的输入:

在 App.vue 中的实现(功能节选)

<script>
// 方向键的键盘按键的编码
const directionKeys = {
 up: 38,
 down: 40,
 left: 37,
 right: 39,
};

// 检查是否在水平方向上
function checkIsLevel(direction) {
 return direction === directionKeys.right || direction === directionKeys.left;
}

export default {
 data () {
   return {
     // 当前的方向键的编码
     direction: undefined,
     // 最终输入的方向键的编码
     lastInputDirection: undefined,
   }
 }
 mounted() {
   window.addEventListener('keydown', this.onKeydown);
 },
 methods: {
   onKeydown(e) {
     if (
       // 检查是否为方向键
       ![38, 40, 37, 39].includes(keyCode) ||
       // 检查是否在同一个水平方向上
       checkIsLevel(keyCode) === checkIsLevel(this.direction)
     ) {
       return;
     }
     // 保存输入的方向
     this.lastInputDirection = keyCode;
   },
 },
};
</script>

碰撞检测

游戏要求玩家避免触碰到自身或者边界, 我们自然而然的就需要去检测它们是否发生了碰撞.

检测与自身碰撞的方法是, 判断蛇头的位置是否与蛇身体的位置相同:

// 检测是否发生碰撞
function isRepeat(list, pos) {
 return list.some(({ pos: itemPos }) => pos.x === itemPos.x && pos.y === itemPos.y);
}

// 使用的地方传入蛇身体数组和蛇头的位置
isRepeat(snakeBodyList, headerPos);

而检测与边界碰撞的方法是, 判断蛇头的位置是否超出了游戏区域:

const MAX_X = 500;
const MAX_Y = 500;

// 检测是否超出边界
function isCrossedLine(x, y) {
 // 因为是使用position, 我们的位置计算需要考虑到 { x: 0, y: 0 } 的位置不为边界
 return x >= MAX_X || x < 0 || y >= MAX_Y || y < 0;
}

当蛇头的位置将要超出了游戏区域或者与蛇身体的位置相同时, 游戏结束:

const next = updatePos(this.headerPos, this.direction);
if (isCrossedLine(next.x, next.y) || isRepeat(this.snakeBodyList, next)) {
 alert('你输了');
 return;
}

实现渲染动画

为了写出渲染动画, 我们需要尝试理解蛇的运动方式.

当玩家输入操作的时候, 蛇会根据用户输入的方向进行移动, 在这个过程中蛇头的位置会发生变化, 而蛇身体的位置也会随之发生变化. 仔细观察可以发现, 其实不断变化的每个蛇身就是将它的位置替换成上一个蛇身的位置:

let head = this.headerPos;
const snakeBodyList = this.snakeBodyList;
for (const body of snakeBodyList) {
 const nextPos = body.pos;
 body.pos = head;
 head = nextPos;
}

除了这种逐步更新的方式也可以使用更简单的直接更新数组的方式, 比如:

这样会使 uuid 无法更新, vue 不会重新渲染 DOM, 导致 transition 无法生效

// 移除蛇尾
const snakeBodyList = this.snakeBodyList.slice(0, this.snakeBodyList.length - 1);
// 添加当前的蛇头至蛇身的最前方
snakeBodyList.unshift({ pos: this.headerPos, uuid: this.id++ });

而当蛇头触碰到豆的时候, 豆会被消除并且延长蛇身:

if (isRepeat(snakeBodyList, this.foodPos)) {
 snakeBodyList.push(this.createBody());
}

有了检测逻辑, 我们再将动画添加上. 因为蛇是一步一步的移动, 所以可以使用 setTimeout 来实现动画:

render 函数最终会挂载在 vue 实例上

function render() {
 const next = updatePos(this.headerPos, this.lastInputDirection);
 if (isCrossedLine(next.x, next.y) || isRepeat(this.snakeBodyList, next)) {
   clearTimeout(this._timer);
   alert('你输了');
   return;
 }
 const snakeBodyList = this.snakeBodyList.slice(0, this.snakeBodyList.length - 1);
 snakeBodyList.unshift({ pos: this.headerPos, uuid: this.id++ });
 this.headerPos = next;
 this.lastPos = snakeBodyList[snakeBodyList.length - 1].pos;
 if (isRepeat(snakeBodyList, this.foodPos)) {
   snakeBodyList.push(this.createBody());
 }
 this.snakeBodyList = snakeBodyList;
 this.direction = this.lastInputDirection;
 this._timer = setTimeout(() => this.render(), 100);
}

最后的润色

我们添加一下生成豆的方法, 并且保证它的位置不会出现在游戏区域的边界或者蛇身体的位置上:

genFoot 函数最终会挂载在 vue 实例上

// 生成随机数
function genRandom(max, start) {
 return start + (((Math.random() * (max - start)) / start) >>> 0) * start;
}

// 随机生成豆的位置
function genFoot() {
 const x = genRandom(MAX_X, defaultUnit);
 const y = genRandom(MAX_Y, defaultUnit);
 // 如果出现在游戏区域的边界或者蛇身体的位置上则重新生成
 if (isRepeat(this.snakeBodyList, { x, y }) || isCrossedLine(x, y)) {
   this.genFoot();
 } else {
   this.foodPos = { x, y };
 }
}

// 添加到render方法中
function render() {
 // ...
 if (isRepeat(snakeBodyList, this.foodPos)) {
   snakeBodyList.push(this.createBody());
   this.genFoot();
 }
 // ...
}

再添加一下开始与结束游戏, 以及一些展示当前蛇的信息的地方:

在 App.vue 中的实现(功能节选)

<template>
 <div class="game-box">
   <div class="tools">
     <button @click="playGame">
       {{ isPlaying ? '停止' : isLose ? '重新开始' : '开始' }}
     </button>
     <div class="info-bar">
       <p>🐍 的长度: {{ snakeBodyList.length }}</p>
     </div>
     <p class="count">得分: {{ count }}</p>
   </div>
 </div>
</template>
<script>
export default {
 data: () => ({
   // 游戏状态
   isPlaying: false,
   // 是否失败
   isLose: false,
   // 蛇的步行速度
   speed: 100,
 }),
 methods: {
   playGame() {
     if (this.isPlaying) {
       clearTimeout(this._timer);
     } else {
       this.isLose = false;
       this.init();
       this.genFoot();
       this.render();
     }
     this.isPlaying = !this.isPlaying;
   },
 },
};
</script>

这样我们就使用 vue 实现了一个简单的贪吃蛇游戏了.

效果图

基于Vue实现简单的贪食蛇游戏

来源:https://juejin.cn/post/7088872374019293197

标签:Vue,贪食蛇,游戏
0
投稿

猜你喜欢

  • 生成静态页面的php函数,php爱好者站推荐

    2024-05-03 15:50:09
  • Python实现根据日期获取当天凌晨时间戳的方法示例

    2023-08-04 14:16:46
  • 谈谈Python中的while循环语句

    2023-08-02 19:07:56
  • Frontpage中网页字体的美化研究

    2008-03-10 12:13:00
  • python字典多键值及重复键值的使用方法(详解)

    2023-03-18 14:23:15
  • WSC脚本部件技术:利用Javascript编写ActiveX控件

    2008-05-05 13:13:00
  • Django3基于WebSocket实现WebShell的详细过程

    2021-12-01 21:34:31
  • Go语言中map使用和并发安全详解

    2024-04-26 17:21:00
  • 解决python升级引起的pip执行错误的问题

    2021-09-16 18:24:17
  • Python教程之Python多态的深层次理解

    2021-07-30 07:50:53
  • Python重试库 Tenacity详解(推荐)

    2021-07-07 00:20:09
  • oracle 存储过程和函数例子

    2009-08-08 22:27:00
  • Python3 解释器的实现

    2023-08-09 17:08:53
  • 简单的抓取淘宝图片的Python爬虫

    2022-01-19 14:42:31
  • NCCL深度学习Bootstrap网络连接建立源码解析

    2022-02-25 22:28:10
  • 浅谈Python处理json字符串为什么不建议使用eval()

    2023-08-17 06:56:34
  • JS使用百度地图API自动获取地址和经纬度操作示例

    2024-04-23 09:31:24
  • 详解MySQL中concat函数的用法(连接字符串)

    2024-01-18 09:30:56
  • django中瀑布流写法实例代码

    2022-08-04 11:11:26
  • tensorboard实现同时显示训练曲线和测试曲线

    2023-05-18 04:55:34
  • asp之家 网络编程 m.aspxhome.com