基于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 实现了一个简单的贪吃蛇游戏了.
效果图
来源:https://juejin.cn/post/7088872374019293197


猜你喜欢
- 1、利用Python中的random模块中的choice方法random.choice()可以从任何序列,比如list列表中,选取一个随机的
- 本文实例讲述了Python实现的根据IP地址计算子网掩码位数功能。分享给大家供大家参考,具体如下:#!/usr/bin/env python
- 描述int函数可以将一个指定进制的数字型字符串或者十进制数字转化为整形。语法int(object, base)名称说明备注object一个数
- 背景上周公司培训了MySQL replication, 这个周末打算用所学来实践操作一下。Master server:MySQL conta
- 前言嗨嗨,大家晚上好 ~ 又来给你们分享小妙招啦在python列表有重复元素时,可以有以下几种方式进行删除觉得不错的话,赶紧学起来用用吧 !
- Golang精编100题能力模型(测试)初级primary:熟悉基本语法,能够看懂代码的意图;在他人指导下能够完成用户故事的开发,编写的代码
- SQL Server 2008“阻止保存要求重新创建表的更改”的错误的解决方案是本文我们主要要介绍的内容,情况是这样的:我们在用SQL Se
- 在之前的文章中,我们介绍了PyQt5和PySide2中主窗口控件MainWindow的使用、窗口控件的4中基础布局管理。从本篇开始,我们来了
- 1、psutil是一个跨平台库(https://github.com/giampaolo/psutil)能够实现获取系统运行的进程和系统利用
- 前言这篇文章给大家讲解的是在vue-cli脚手架中如何配置vue-router前端路由的相关内容,分享出来供打击参考学习,下面来一起看看详细
- 废话不多说了,具体代码如下所示:<html><head>< >function selectAll(){
- 一、configparser模块是什么可以用来操作后缀为 .ini 的配置文件;python标准库(就是python自带的意思,无需安装)二
- //清空form选择 function clearForm(id){ var formObj = document.getElementBy
- 第一种方法:select *from ( select Url,case when Month=01 then&nb
- 优先级两者放置相同条件,之所以可能会导致结果集不同,就是因为优先级。on的优先级是高于where的。首先明确两个概念:LEFT JOIN 关
- 新闻系统,相册系统可以用用哦,简单实用,有兴趣的可以自己扩充!^_^相册截图:<?xml version="1.0"
- 前言本文主要是用 cpu 版本的 tensorflow 2.1 搭建深度学习模型,完成对电影评论的情感分类任务。 本次实践的数据来源于IMD
- 本月第一天日期SELECT FirstDayOfCurrentMonth = dateadd(mm,datediff(mm,0,getdat
- 浏览器对于CSS的支持问题落后于CSS的发展,以占有市场绝对份额的Internet Explorer来说,直到其前不久发布的第8个版本才刚刚
- 自定义可迭代的类列表可以获取列表的长度,然后使用变量i对列表索引进行循环,也可以获取集合的所有元素,且容易理解。没错,使用列表的代码是容易理