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

news2024/11/24 15:31:25

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

文章目录

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

三、截图展示

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

四、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1123462.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信h5支付配置,商家存在未配置的参数,请联系商家解决

对于PC端来说&#xff0c;只需要开通 native支付 就可以了 但手机端h5还需要配置支付域名&#xff0c;并且域名只需要配置一级就可以了&#xff0c;比如&#xff1a;a.test.com, b.test.com, 只需要配置 test.com 就能满足所有的二级域名了, 而不需要配置a.test.com或者b.te…

nmp、yarn、yeoman、bower是什么东西?

1&#xff1a;npm&#xff08;Node Package Manager&#xff09;&#xff1a;npm 是 Node.js 的包管理器&#xff0c;用于安装、管理和共享 JavaScript 包。它是 JavaScript 生态系统中最常用的包管理工具&#xff0c;可以轻松地安装和管理项目的依赖项。 2&#xff1a;Yarn&a…

远程桌面无法复制粘贴文件

本地通过mstsc连接到远程说面后&#xff0c;无法把本地文件复制到远程桌面上或者远程桌面的文件无法复制到本地机器修改。 思路:重启rdpclip服务 1、远程说面打开任务管理器 查看到rdpclip.exe程序&#xff0c;如果存在就关闭掉&#xff0c;不存在就跳过 2、winR打开运行&am…

2023 年 12 款最佳免费 PDF 阅读器

12 大最佳免费 PDF 阅读器 PDF 阅读器是一种可以打开 PDF 文件的软件&#xff0c;PDF 文件可能是最流行的文档格式。尽管 PDF 文件已经存在超过 25 年&#xff0c;但它仍然是 Internet 上文档的主要格式。但是&#xff0c;要打开此类文档&#xff0c;您必须在计算机上下载指定…

逐秒追加带序号输入当前时间:fgets fputs sprintf fprintf

//向某文件中逐秒追加带序号输入当前时间 #include<stdio.h> #include<stdlib.h> #include<time.h> #include<string.h> #include <unistd.h> int main(int argc, char const *argv[]) { time_t tv; // time(&tv);//法1:获取秒数 …

css flex实现同行div根据内容高度自适应且保持一致

有情况如下&#xff1a;三个div的高度是随着内容自适应的&#xff0c;但希望每列的高度都相同&#xff0c;即&#xff0c;都与最高的相同。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewp…

同为科技(TOWE)机架PDU产品在IDC数据中心机房建设中的应用

当今社会互联网发展迅速&#xff0c; 随着带宽需求的提升&#xff0c; 网络的保密性、安全性的要求就越来越迫切。PDU(Power Distribution Unit) 是 PDU具备电源分配和管理功能的电源分配管理器。PDU电源插座是多有设备运行的第一道也是最为密切的部件&#xff0c; PDU的好坏直…

VS Code里使用Debugger for Unity插件进行调试(2023最新版)

vs Code因为免费启动&#xff08;飓快&#xff09;的原因&#xff0c;已经成为事实的第一首选的IDE了 比sublime还快很多很多&#xff0c;其他特性包括但不限于&#xff1a; 最直接的github拉取&#xff0c;提交管理插件管理支持各种语言的识别代码分屏 是毫无疑问的第一编程…

跨境商城源码可以支持多店铺管理模式吗?

跨境商城源码是一种用于搭建跨境电商平台的模板或框架。它为开发者提供了一套完整的程序代码&#xff0c;可快速建立一个功能齐备的跨境商城网站。跨境商城源码通常具备商品展示、购物车、支付、订单管理等基础功能&#xff0c;并支持二次开发和定制。 什么是多店铺管理模式? …

忆联SR-IOV解决方案:助力云数据中心节能提效,向“绿”而行

随着AI时代的到来&#xff0c;云数据中心如何实现节能提效正成为热门话题。其中&#xff0c;SR-IOV技术凭借灵活度高以及可节约虚拟化业务算力等优势&#xff0c;是打造绿色低碳云数据中心的重要解决方案之一。 一、什么是SR-IOV 技术 SR-IOV 是由国际组织 PCI-SIG 组织定义的…

AQS是什么?AbstractQueuedSynchronizer之AQS原理及源码深度分析

文章目录 一、AQS概述1、什么是AQS2、技术解释3、基本原理4、AQS为什么这么重要 二、AQS数据结构1、AQS的结构2、ReentrantLock与AbstractQueuedSynchronizer3、AQS的state变量4、AQS的队列5、AQS的Node&#xff08;1&#xff09;Node的waitStatus&#xff08;2&#xff09;属性…

UWB工业现场数字化管理

资产管理数字化 器具场内外流转跟踪管理 无动力资产跟踪 大件地堆及成品固定区域盘点 大型资产移动/流动盘点 成品及返修车定位 无人值守人、车、物出入监测 资产移动盘点 安全生产数字化 危险区域人员/物资管理 叉车防碰撞及安全运行管理 行车防碰撞及安全运行管理 …

SLAM从入门到精通(robot上层软件开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们了解到&#xff0c;ros系统基本上都是依赖于ubuntu和linux来运行的。如果ros是只是跑在机器人的系统上面&#xff0c;不涉及到上层交互&am…

LeetCode:1402. 做菜顺序、2316. 统计无向图中无法互相到达点对数

1. 1402 做菜顺序 题目详细为&#xff1a; 一个厨师收集了他 n 道菜的满意程度 satisfaction &#xff0c;这个厨师做出每道菜的时间都是 1 单位时间。 一道菜的 「 like-time 系数 」定义为烹饪这道菜结束的时间&#xff08;包含之前每道菜所花费的时间&#xff09;乘以这道菜…

外汇天眼:本周无牌裸奔平台名单出炉,你踩“坑”了么?!!

监管信息早知道&#xff01;外汇天眼将每周定期公布监管牌照状态发生变化的交易商&#xff0c;以供投资者参考&#xff0c;规避投资风险。如果平台天眼评分过高&#xff0c;建议投资者谨慎选择&#xff0c;因为在外汇天眼评分高不代表平台没问题&#xff01; 以下是监管牌照发生…

Gartner发布2024 年十大战略技术趋势

10月17日&#xff0c;Gartner 发布2024年企业机构需要探索的****十大战略技术趋势。Gartner研究副总裁Bart Willemsen表示&#xff1a;“由于技术变革以及社会经济方面的不确定性&#xff0c;我们必须大胆采取行动并从战略上提高弹性&#xff0c;而不是采取临时措施。IT领导者的…

塔望3W消费战略全案丨九代拉祜人,一饼古树茶

存木香 客户&#xff1a;云南双江存木香茶叶商贸有限公司 品牌&#xff1a;存木香 时间&#xff1a;2019年 &#xff08;项目部分内容保密期&#xff09; 沧海桑田 存木香依然 存木香 CUNMUXIANG( 全称云南双江存木香茶业有限公司 ), 成立于2011 年 , 总部设于北回归线横…

小红书内测「群AI」功能;大模型技术图谱;曾鸣「看十年」智能商业演讲实录;GPT最佳实践-大白话编译版 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f525; 小红书内测「群AI」功能&#xff0c;可以创建虚拟角色并加入群聊 小红书正在内测「群AI」新功能。日报根据站内分享贴拼出了一份教程&…

C++并发编程(1)-- 多线程的基础知识

1 进程、线程和协程 &#xff08;1&#xff09;进程 进程可以简单理解成正在执行的一个程序&#xff0c;比如你电脑上运行的QQ、360杀毒软件等就是进程。 进程是程序资源管理的最小单位。 进程会管理那些资源呢&#xff1f;通常包括内存资源、IO资源、信号处理等。 这里对…

众和策略:华为汽车概念活跃,圣龙股份斩获12板,华峰超纤涨10%

华为轿车概念23日盘中再度生动&#xff0c;到发稿&#xff0c;华峰超纤涨超10%&#xff0c;佛山照明、圣龙股份、隆基机械、银宝山新等涨停&#xff0c;赛力斯涨近6%。 值得注意的是&#xff0c;圣龙股份已接连12个交易日涨停。 昨日晚间&#xff0c;圣龙股份宣布前三季度成果…