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

news/2024/7/19 14:07:51 标签: sass, 前端, js, typescript

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、逻辑设计分析
  • 二、代码实现
    • 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

代码如下(示例):

typescript">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;
            }
        }
    }
}

三、截图展示

在这里插入图片描述
在这里插入图片描述

四、总结

实现贪吃蛇小游戏使用的技术有为了使用而使用的嫌疑,使用还有些不太熟练,望大家多多理解,如有建议欢迎多多评论或私信指教。


http://www.niftyadmin.cn/n/5116737.html

相关文章

逐字稿 | 视频理解论文串讲(下)【论文精读】

1 为什么研究者这么想把这个双流网络替换掉&#xff0c;想用3D 卷积神经网络来做&#xff1f; 大家好&#xff0c;上次我们讲完了上半部分&#xff0c;就是 2D 网络和一些双流网络以及。它们的。变体。今天我们就来讲一下下半部分&#xff0c;就是 3D 网络和 video Transformer…

MATLAB算法实战应用案例精讲-【人工智能】ROS机器人

目录 ROS机器人操作系统底层原理 1、序列化 1.1 什么是序列化? 1.2 ROS中的序列化实现

【机器学习合集】激活函数合集 ->(个人学习记录笔记)

文章目录 1. S激活函数(sigmoid&Tanh)2. ReLU激活函数3. ReLU激活函数的改进4. 近似ReLU激活函数5. Maxout激活函数6. 自动搜索的激活函数Swish 1. S激活函数(sigmoid&Tanh) Sigmoid函数在机器学习中经常用作激活函数&#xff0c;但它在某些情况下容易出现梯度消失问题…

adb: error: 46-byte write failed: Invalid argument

使用adb pull可以&#xff0c;但是adb push fail。 貌似是adb的问题&#xff0c;将adb.exe 重命名为pdb&#xff0c;使用pdb push就行了。

基于STM32+华为云设计的智慧农业灌溉系统(2023升级版)

一、设计需求 基于STM32+华为云设计的智慧农业灌溉系统 【1】 项目背景 随着全球人口的不断增长和城市化进程的加速,农业生产面临着越来越大的挑战。作为传统的行业,农业生产需要不断地革新创新,以适应现代社会对食品安全、环境保护和资源利用的要求。同时,气候变化等因素…

React 框架

1、React 框架简介 1.1、介绍 CS 与 BS结合&#xff1a;像 React&#xff0c;Vue 此类框架&#xff0c;转移了部分服务器的功能到客户端。将CS 和 BS 加以结合。客户端只用请求一次服务器&#xff0c;服务器就将所有js代码返回给客户端&#xff0c;所有交互类操作都不再依赖服…

百科词条有哪些类型?百科词条的创建条件

大家最常见的百科词条有品牌词条和企业词条及产品词条&#xff0c;其实还有不少词条类型。百科词条到底有哪些类型呢&#xff1f;小马识途营销顾问在本文分享。 1、企业百科 以企业名称为词条名称创建的百科词条为企业百科&#xff0c;企业百科最为普遍&#xff0c;只要公司注册…

数据库MySQL(二):DDL数据定义语言

数据定义语言&#xff08;Data Definition Language&#xff0c;DDL&#xff09; 该语言主要用于定义数据库对象&#xff0c;操作对象为数据库、表或字段。 数据库操作 # 查询所有数据库 SHOW DATABASES;# 查询当前数据库 SELECT DATABASE(); # 创建数据库 CREATE DATABASE […