提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、逻辑设计分析
- 二、代码实现
- 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 = 500
return baseSpeed - score.value * ((500 - 40) / gridData.length)
})
const snakeMove = () => {
let nextItem:Ad.Game.Snake.BaseItem | null = null
const 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 = null
break;
case 'bottom':
nextItem = gridData[curLastSpot.index + row]
break;
case 'left':
nextItem = gridData[curLastSpot.index - 1]
if(nextItem.rowNum !== curLastSpot.rowNum) nextItem = null
break;
default:
console.error('方向错误:', direction.value)
}
if(nextItem?.isSnake || !nextItem){
gameOver()
} else {
updateSnake(nextItem)
}
}
const windowKeyDown = (ev:KeyboardEvent) => {
const { keyCode } = ev
ev.preventDefault()
switch(keyCode){
case 37: //左键
if(direction.value === 'right') return
else direction.value = 'left'
break;
case 38://上键
if(direction.value === 'bottom') return
else direction.value = 'top'
break;
case 39://右键
if(direction.value === 'left') return
else direction.value = 'right'
break;
case 40://下键
if(direction.value === 'top') return
else direction.value = 'bottom'
break;
default:
console.log('keyCode:', keyCode)
}
snakeMove()
}
/** 设置运动时间更新机制 */
const setSpeedTime = () => {
if(!curState.value) return
if(timeStampMove)clearTimeout(timeStampMove)
timeStampMove = setTimeout(() => {
snakeMove()
setSpeedTime()
}, speed.value)
}
/** 开始游戏 */
const startGame = () => {
window.addEventListener('keydown', windowKeyDown)
initNewSpot()
curState.value = true
setSpeedTime()
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 = 0
snakeData.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 * column
for (let index = 0; index < allGridNums; index++) {
let currentRow = Math.ceil((index + 1) / row)
let currentColumn = (index+1) % row
let 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 = true
if(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].index
gridData[preIdx].isHead = false
snakeData.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 = 0
snakeData.value = []
direction.value = 'right'
initGrid()
})
return
}
gridData[nextItem.index].isHead = true
gridData[nextItem.index].isSnake = true
if(!nextItem.newSpot) {
const delItme:Ad.Game.Snake.BaseItem = snakeData.value.shift() as Ad.Game.Snake.BaseItem
gridData[delItme.index].isSnake = false
} else {
gridData[snakeData.value[snakeData.value.length - 1].index].newSpot = false
initNewSpot()
}
}
/** 生成新的目标得分点 */
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;
}
}
}
}
三、截图展示
四、总结
实现贪吃蛇小游戏使用的技术有为了使用而使用的嫌疑,使用还有些不太熟练,望大家多多理解,如有建议欢迎多多评论或私信指教。