自学系列之小游戏---贪吃蛇(vue3+ts+vite+element-plus+sass)(module.scss + tsx)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、逻辑设计分析
- 二、代码实现
- 1.TS interface
- 2.javascript
- 3.页面样式(Sass)
- 三、截图展示
- 四、总结
前言
主要技术如下:vue3 、vite 、ts、element-plus 、 tsx、sass、xxx.module.scss
一、逻辑设计分析
数据定义定义
index : 总下标
colNum : 列数
colIdx : 列下标
rowNum : 行数
rowIdx : 行下标
isSnake: 是否属于模型内容
isHead : 是否属于模型头部
isTail : 是否属于模型尾部
生成网格
1、生成整体网格数据单位
2、生成所有行数据
3、生成所有列数据
初始化snake
1、生成默认snake长度
2、生成默认snake位置
3、定义snake首尾
速度
1、定义初始速度
2、定义加速度模型
生成目标
1、随机生成
2、新增eat目标单元* 生成时排除snake模型部分,随机生成位置
snake移动逻辑
1、移动UI方案* 网格背景变化* 变化速度取移动速度间隔值
2、移动方向方案* 记录移动方向 默认向右* 当前方向向右`向上取当前snake头部行数加一与行下标取下一次位移目标位置为头部数据改变前进方向为向上撞自己身体判断向右操作无效向右撞墙判断撞自己身体判断(后期加入向右加速度,keydown加速度,keyup取消加速度)向下取当前snake头部行数减一与行下标取下一次位移目标位置为头部数据改变前进方向为向下撞自己身体判断向左操作无效`* 当前方向向左`向上取当前snake头部行数加一与行下标取下一次位移目标位置为头部数据改变前进方向为向上撞自己身体判断向右操作无效向下取当前snake头部行数减一与行下标取下一次位移目标位置为头部数据改变前进方向为向下撞自己身体判断向左操作无效向左撞墙判断撞自己身体判断(后期加入向左加速度,keydown加速度,keyup取消加速度)`* 当前方向向上`向上操作无效向上撞墙判断撞自己身体判断(后期加入向上加速度,keydown加速度,keyup取消加速度)向右取当前snake头部列数加一与列下标取下一次位移目标位置为头部数据改变前进方向为向右撞自己身体判断向下操作无效向左操作取当前snake头部列数减一与列下标取下一次位移目标位置为头部数据改变前进方向为向左撞自己身体判断`* 当前方向向下`向上操作无效向右取当前snake头部列数加一与列下标取下一次位移目标位置为头部数据改变前进方向为向右撞自己身体判断向下操作无效向下撞墙判断撞自己身体判断(后期加入向上加速度,keydown加速度,keyup取消加速度)向左操作取当前snake头部列数减一与列下标取下一次位移目标位置为头部数据改变前进方向为向左撞自己身体判断`
游戏结束判断
1、撞墙死亡
2、撞自己身体死亡
3、身体占满网格游戏通关
分数计算
1、身总长度减去初始身体长度
二、代码实现
1.TS interface
代码如下(示例):
declare namespace Ad{namespace Game{namespace Snake {interface BaseItem {/** 下标 */index:number,/** 列数 */colNum:number,/** 列坐标 */colIdx:number,/** 行数 */rowNum:number,/** 行坐标 */rowIdx:number,/** 是否属于目标得分点 */newSpot:boolean,/** 是否属于模型内容 */isSnake:boolean,/** 是否属于模型头部 */isHead:boolean,/** 是否属于模型尾部 */isTail:boolean}}}
}
2.javascript
代码如下:
import { Ref, computed, defineComponent, reactive, ref } from "vue";
import SnakeScss from './greedySnake.module.scss'
import '../game.scss'
import { ElMessageBox } from 'element-plus'
import { secondsToDate } from "@/utils/utils";export default defineComponent({setup(props, ctx) {/** 行-基点数 */const row = 29/** 列-基点数 */const column = 29/** 时间 */let timer = ref(0)/** 当前游戏状态 */let curState = ref(false)/** 贪吃蛇数据模型 */let snakeData:Ref<Array<Ad.Game.Snake.BaseItem>> = ref([])/** 时间记录器 */let timeStamp: undefined | NodeJS.Timer;/** 速度移动记录器 */let timeStampMove: undefined | NodeJS.Timer;/** 前进方向 */let direction = ref('right')/** 能用做得分点的框 */let canCreatStops:Ref<Array<number>> = ref([])/** 得分 */let score = computed(() => snakeData.value.length - 5 )/** 移动速度 */let speed = computed(() => {const baseSpeed = 500return baseSpeed - score.value * ((500 - 40) / gridData.length)})const snakeMove = () => {let nextItem:Ad.Game.Snake.BaseItem | null = nullconst curLastSpot = snakeData.value[snakeData.value.length-1]switch(direction.value){case 'top':nextItem = gridData[curLastSpot.index - row]break;case 'right':nextItem = gridData[curLastSpot.index + 1]if(nextItem.rowNum !== curLastSpot.rowNum) nextItem = nullbreak;case 'bottom':nextItem = gridData[curLastSpot.index + row]break;case 'left':nextItem = gridData[curLastSpot.index - 1]if(nextItem.rowNum !== curLastSpot.rowNum) nextItem = nullbreak;default:console.error('方向错误:', direction.value)}if(nextItem?.isSnake || !nextItem){gameOver()} else {updateSnake(nextItem)}}const windowKeyDown = (ev:KeyboardEvent) => {const { keyCode } = evev.preventDefault()switch(keyCode){case 37: //左键if(direction.value === 'right') returnelse direction.value = 'left'break;case 38://上键if(direction.value === 'bottom') returnelse direction.value = 'top'break;case 39://右键if(direction.value === 'left') returnelse direction.value = 'right'break;case 40://下键if(direction.value === 'top') returnelse direction.value = 'bottom'break;default:console.log('keyCode:', keyCode)}snakeMove()}/** 设置运动时间更新机制 */const setSpeedTime = () => {if(!curState.value) returnif(timeStampMove)clearTimeout(timeStampMove)timeStampMove = setTimeout(() => {snakeMove()setSpeedTime()}, speed.value)}/** 开始游戏 */const startGame = () => {window.addEventListener('keydown', windowKeyDown)initNewSpot()curState.value = truesetSpeedTime()timeStamp = setInterval(() => timer.value += 1, 1000)}/** 重置 */const resetGame = () => {window.removeEventListener('keydown', windowKeyDown)clearInterval(timeStamp)clearTimeout(timeStampMove)curState.value = false}/** 游戏结束 */const gameOver = () => {resetGame()ElMessageBox.confirm(`本次存活时间${secondsToDate(timer.value, 'HH*mm*ss', true )},本次得分${score.value},再接再厉。`,'游戏结束',{confirmButtonText: 'OK',showCancelButton:false,showClose:false,closeOnClickModal:false,closeOnPressEscape:false,type: 'warning',}).then(res => {timer.value = 0snakeData.value = []direction.value = 'right'initGrid()})}/** 得分、计时、操作面板生成 */const renderHeader = () => {return <div class='header'><div class='header-item'><div>时间:</div><div class='number-value'>{ secondsToDate(timer.value, 'HH:mm:ss', true) || '00:00:00' }</div></div><div class='header-item'><div>得分:</div><div class='number-value'>{ score.value || 0 }</div></div><div class='header-item'>{curState.value ? <div class='btn' onClick={ gameOver }>结束游戏</div> : <div class='btn' onClick={ startGame }>开始游戏</div>}</div></div>}/** 网格数据 */let gridData:Array<Ad.Game.Snake.BaseItem> = reactive([])const renderGrid = () => {return <div class={SnakeScss['snake-grid']}>{ gridData.map(gridItem => renderGridItem(gridItem)) }</div>}const renderGridItem = (gridItem:Ad.Game.Snake.BaseItem) => {const activeClass = gridItem.isSnake ? SnakeScss['grid-item_active'] : ''let directionClass:{[key:string]:string} = {top:'snake-title_top',right:'snake-title_right',bottom:'snake-title_bottom',left:'snake-title_left'}const headClass = gridItem.isHead ? SnakeScss[directionClass[direction.value]] : ''const newSpotClass = gridItem.newSpot ? SnakeScss['grid-item_new-spot'] : ''return <div class={[SnakeScss['grid-item'], activeClass, headClass, newSpotClass] }>{gridItem.isHead && [<div class={SnakeScss['eye']}></div>,<div class={SnakeScss['eye']}></div>]}</div>}/** 初始化网格 */const initGrid = () => {gridData = []const allGridNums = row * columnfor (let index = 0; index < allGridNums; index++) {let currentRow = Math.ceil((index + 1) / row)let currentColumn = (index+1) % rowlet currentGrid:Ad.Game.Snake.BaseItem = {index:index,rowNum:currentRow,rowIdx:currentColumn,colNum:currentColumn,colIdx:currentRow,newSpot:false,isSnake:false,isHead:false,isTail:false}gridData.push(currentGrid)}initSnakePosition()}/** 初始化贪吃蛇数据模型 */const initSnakePosition = () => {const rowMidDrop = Math.ceil(row / 2)const columnMidDrop = Math.ceil(column / 2)const mindSpotIdx = ((rowMidDrop - 1) * row) + (columnMidDrop - 1)const snakeDefaultSpotIdxs = [mindSpotIdx-2, mindSpotIdx-1, mindSpotIdx, mindSpotIdx+1, mindSpotIdx+2]for (let index = 0; index < snakeDefaultSpotIdxs.length; index++) {gridData[snakeDefaultSpotIdxs[index]].isSnake = trueif(index === 0){gridData[snakeDefaultSpotIdxs[index]].isTail = true}if(index === snakeDefaultSpotIdxs.length - 1){gridData[snakeDefaultSpotIdxs[index]].isHead = true}snakeData.value.push(gridData[snakeDefaultSpotIdxs[index]])}}/*** 更新贪吃蛇位置和长度* @param nextItem 下一个目标位置*/const updateSnake = (nextItem:Ad.Game.Snake.BaseItem) => {const preIdx = snakeData.value[snakeData.value.length - 1].indexgridData[preIdx].isHead = falsesnakeData.value.push(nextItem)if(snakeData.value.length == gridData.length){resetGame()ElMessageBox.confirm(`本次存活时间${secondsToDate(timer.value, 'HH*mm*ss', true )},本次得分${score.value},太厉害了,游戏通关。`,'通关',{confirmButtonText: 'OK',showCancelButton:false,showClose:false,closeOnClickModal:false,closeOnPressEscape:false,type: 'success',}).then(res => {timer.value = 0snakeData.value = []direction.value = 'right'initGrid()})return}gridData[nextItem.index].isHead = truegridData[nextItem.index].isSnake = trueif(!nextItem.newSpot) {const delItme:Ad.Game.Snake.BaseItem = snakeData.value.shift() as Ad.Game.Snake.BaseItemgridData[delItme.index].isSnake = false} else {gridData[snakeData.value[snakeData.value.length - 1].index].newSpot = falseinitNewSpot()}}/** 生成新的目标得分点 */const initNewSpot = () => {const snakeDataIdxs = snakeData.value.map(item => item.index)canCreatStops.value = gridData.filter(item => !snakeDataIdxs.includes(item.index)).map(item => item.index)const idx = Number((Math.random() * canCreatStops.value.length - 1).toFixed(0))gridData[canCreatStops.value[idx]].newSpot = true}initGrid()return () => <div class='game-content'>{ renderHeader() }{ renderGrid() }</div>}
})
3.页面样式(Sass)
greedySnake.module.scss
.snake-grid{display: flex;flex-wrap: wrap;border: 1px solid #eee;border-radius: 4px;margin: 0 auto;margin-top: 20px;width: 580px;.grid-item{width: 20px;height: 20px;.eye{width: 5px;height: 5px;background-color: #fff;border-radius: 50%;margin-left: 10px;margin-top: 3px;}}.grid-item:nth-child(2n){background-color:rgb(248, 248, 248)}.grid-item:nth-child(2n-1){background-color:rgb(255, 255, 255)}.grid-item_active{background-color: rgb(0, 0, 0) !important;}.snake-title_top{border-top-right-radius: 50%;border-top-left-radius: 50%;}.snake-title_right{border-top-right-radius: 50%;border-bottom-right-radius: 50%;}.snake-title_bottom{border-bottom-right-radius: 50%;border-bottom-left-radius: 50%;}.snake-title_left{border-top-left-radius: 50%;border-bottom-left-radius: 50%;}.grid-item_new-spot{background-color: rgb(0, 0, 0) !important;}
}
game.scss
.game-content{width: 900px;margin: 0 auto;height: 700px;margin-top: 40px;box-shadow: 0px 1px 10px 4px #ccc;border-radius: 10px;user-select: none;.header{display: flex;justify-content: space-around;height: 60px;border-bottom: 1px solid #eee;margin: 0 20px;align-items: center;.header-item{display: flex;align-items: center;font-weight: bold;font-size: 16px;.number-value{font-size: 18px;}.btn{width: 120px;border-radius: 10px;height: 36px;text-align: center;line-height: 36px;box-shadow: 0px 1px 10px 0px #ccc;cursor: pointer;}}}
}
三、截图展示
四、总结
实现贪吃蛇小游戏使用的技术有为了使用而使用的嫌疑,使用还有些不太熟练,望大家多多理解,如有建议欢迎多多评论或私信指教。
相关文章:

自学系列之小游戏---贪吃蛇(vue3+ts+vite+element-plus+sass)(module.scss + tsx)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、逻辑设计分析二、代码实现1.TS interface2.javascript3.页面样式(Sass) 三、截图展示四、总结 前言 主要技术如下:vue3…...
JAVA项目中什么是DTO、DAO、PO、Controller、Common
DTO(Data Transfer Object)和DAO(Data Access Object)是Java中常用的两种设计模式,它们在软件开发中扮演着不同的角色。 1. **DTO (Data Transfer Object)**:数据传输对象,主要用于在远程调用等…...
Alibaba Druid整合
文章目录 方式一:自定义整合方式二:使用 Druid 官方的 Starter Druid官网:https://github.com/alibaba/druidDruid官网文档(中文):https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%…...

SpringCloud 微服务全栈体系(三)
第五章 Nacos 注册中心 国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba 也推出了一个名为 Nacos 的注册中心。 一、认识和安装 Nacos 1. 认识 Nacos Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eure…...

VScode连接的服务器上使用jupyter显示请选择内核源
问题复现 我实在VScode上用ssh-remote连接的服务器,想用.ipynb文件上写东西,结果窗口上方弹出一个输入框,“请键入以选择内核”; 在扩展里找到jupyter更新一下 之前左边的图标是灰色的,后来我下下载了新的版本&#…...

新能源汽车展厅用哪些种类的显示屏比较好?
现在有越来越多的新能源汽车展厅开到了商场、购物中心当中。在新能源汽车展厅中,显示屏已经成为不可或缺的设备设施,可以用来展现产品介绍、优惠信息、文化宣传等。那么新能源汽车展厅的显示大屏用什么屏比较好呢? LED大屏幕:LED显…...

proxmox pve /dev/mapper/pve-root扩容
vgs3 pvs4 vgs5 lvs6 lvremove /dev/pve/data8 lvresize -l 100%FREE /dev/pve/root9 resize2fs /dev/mapper/pve-root 10 history...

【ECS游戏架构】逻辑帧驱动带来的性能和即时性问题分析
1024水一篇~ 个人拙见,如有错误希望大佬拔刀纠正。 根据守望先锋在GDC会议上对ECS架构的描述,所有的系统(system)都是由逻辑帧驱动的:每帧遍历所有的system,并调用system的update()更新游戏世界的状态。 在实际应用中这可能会存…...

数据库监控:关键指标和注意事项
【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 听到模糊的说法“我们的数据库有问题”对于任何数据库管理员或管理员来说都是一场噩梦。有时是真的,有时不是,到底问题出在哪里呢?真…...

高防回源ip被源站拦截怎么办
在进行网站运营过程中,我们经常会遇到DDoS攻击等网络安全威胁。为了保护网站的正常运行,很多企业选择使用高防服务来应对这些攻击。有时候我们可能会遇到一个问题,就是高防回源IP被源站拦截的情况。 那么,当我们发现高防回源…...
关于集群和分布式部署
EJB的RPC是同步调用可实现分布式计算,是SessionBean和EntityBean用的,而JMS是异步调用。RMI,和webservice也可以实现分布式计算。 举例说明,假设我们的系统有三个EJB组件:人事、财务、销售,都是开放远程接口…...

XIlinx提供的DDR3 IP与 UG586
DDR系统需要关注的三样东西:控制器、PHY、SDRAM颗粒,但这是实现一个DDR3 IP所需要的,如果只希望调用IP的话,则只需要调用IP即可,目前时间紧急,我先学一学如何使用IP,解决卡脖子的问题࿰…...

C++数据结构X篇_19_排序基本概念及冒泡排序(重点是核心代码)
文章目录 1. 排序基本概念2. 冒泡排序2.1 核心代码2.2 冒泡排序代码2.3 查看冒泡排序的时间消耗2.4 冒泡排序改进版减小时间消耗 1. 排序基本概念 现实生活中排序很重要,例如:淘宝按条件搜索的结果展示等。 概念 排序是计算机内经常进行的一种操作,其目…...
LeetCode LCR 179. 查找总价格为目标值的两个商品
和为 s 的两个数字 题目链接 LCR 179. 查找总价格为目标值的两个商品 购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况,返回任一结果即可。 示例 1: 输入:price [3, 9, 12, …...
上架用的SDK三方应用隐私
SDK名称:华为推送 使用目的:用于向华为手机用户推送消息 使用场景:用户账号相关促销活动、消息提醒更新时 信息收集类型:设备相关信息(Android_ID)使用的敏感权限:不涉及 使用的敏感权限&am…...

从REST到GraphQL:升级你的Apollo体验
前言 「作者主页」:雪碧有白泡泡 「个人网站」:雪碧的个人网站 「推荐专栏」: ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄,vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄ÿ…...
Jupyter使用技巧-环境篇
不同于其他IDE,有时会出现找不到文件路径,通常是因为当前工作目录(working directory)不同所导致的。Jupyter Notebook 会在启动时选择一个初始的工作目录,而这个目录可能与你运行 .py 文件时所在的目录不同。 import…...

软件项目管理【UML-组件图】
目录 一、组件图概念 二、组件图包含的元素 1.组件(Component)->构件 2.接口(Interface) 3.外部接口——端口 4.连接器(Connector)——连接件 4.关系 5.组件图表示方法 三、例子 一、组件图概念…...

npm版本错误——npm ERR! code ERESOLVE 解决方法
起因 项目中echart版本过低,导致某些图表不能正确显示,所以大手一挥,将echart版本从4升级到了5, 再去运行项目的时候 就发现项目报错了 npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! …...

基于卷积神经网络的乳腺癌分类 深度学习 医学图像 计算机竞赛
文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度,召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...

智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...

ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
JS手写代码篇----使用Promise封装AJAX请求
15、使用Promise封装AJAX请求 promise就有reject和resolve了,就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...