自学系列之小游戏---贪吃蛇(vue3+ts+vite+element-plus+sass)(module.scss + tsx)

本文主要是介绍自学系列之小游戏---贪吃蛇(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)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/272883

相关文章

vite搭建vue3项目的搭建步骤

《vite搭建vue3项目的搭建步骤》本文主要介绍了vite搭建vue3项目的搭建步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1.确保Nodejs环境2.使用vite-cli工具3.进入项目安装依赖1.确保Nodejs环境

Nginx搭建前端本地预览环境的完整步骤教学

《Nginx搭建前端本地预览环境的完整步骤教学》这篇文章主要为大家详细介绍了Nginx搭建前端本地预览环境的完整步骤教学,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录项目目录结构核心配置文件:nginx.conf脚本化操作:nginx.shnpm 脚本集成总结:对前端的意义很多

前端缓存策略的自解方案全解析

《前端缓存策略的自解方案全解析》缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,:本文主要介绍前端缓存的自解方案,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、为什么“清缓存”成了技术圈的梗二、先给缓存“把个脉”:浏览器到底缓存了谁?三、设计思路:把“发版”做成“自愈”四、代码

通过React实现页面的无限滚动效果

《通过React实现页面的无限滚动效果》今天我们来聊聊无限滚动这个现代Web开发中不可或缺的技术,无论你是刷微博、逛知乎还是看脚本,无限滚动都已经渗透到我们日常的浏览体验中,那么,如何优雅地实现它呢?... 目录1. 早期的解决方案2. 交叉观察者:IntersectionObserver2.1 Inter

Vue3视频播放组件 vue3-video-play使用方式

《Vue3视频播放组件vue3-video-play使用方式》vue3-video-play是Vue3的视频播放组件,基于原生video标签开发,支持MP4和HLS流,提供全局/局部引入方式,可监听... 目录一、安装二、全局引入三、局部引入四、基本使用五、事件监听六、播放 HLS 流七、更多功能总结在 v

JS纯前端实现浏览器语音播报、朗读功能的完整代码

《JS纯前端实现浏览器语音播报、朗读功能的完整代码》在现代互联网的发展中,语音技术正逐渐成为改变用户体验的重要一环,下面:本文主要介绍JS纯前端实现浏览器语音播报、朗读功能的相关资料,文中通过代码... 目录一、朗读单条文本:① 语音自选参数,按钮控制语音:② 效果图:二、朗读多条文本:① 语音有默认值:②

vue监听属性watch的用法及使用场景详解

《vue监听属性watch的用法及使用场景详解》watch是vue中常用的监听器,它主要用于侦听数据的变化,在数据发生变化的时候执行一些操作,:本文主要介绍vue监听属性watch的用法及使用场景... 目录1. 监听属性 watch2. 常规用法3. 监听对象和route变化4. 使用场景附Watch 的

前端导出Excel文件出现乱码或文件损坏问题的解决办法

《前端导出Excel文件出现乱码或文件损坏问题的解决办法》在现代网页应用程序中,前端有时需要与后端进行数据交互,包括下载文件,:本文主要介绍前端导出Excel文件出现乱码或文件损坏问题的解决办法,... 目录1. 检查后端返回的数据格式2. 前端正确处理二进制数据方案 1:直接下载(推荐)方案 2:手动构造

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

Vue实现路由守卫的示例代码

《Vue实现路由守卫的示例代码》Vue路由守卫是控制页面导航的钩子函数,主要用于鉴权、数据预加载等场景,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、概念二、类型三、实战一、概念路由守卫(Navigation Guards)本质上就是 在路