--- title: "00-创建菜单与游戏界面" created: 2025-12-02 tags: - 项目 aliases: - 创建菜单与游戏界面 --- # 创建菜单与游戏界面 先把”面子工程做好“ 先写前端再写后端 ## 分析页面 ![[2e0dfa28abe2e36c18f52678994e1b42-9d4eb92f.png]] 不外乎分为两个部分 导航栏和中心内容板块 而每个页面的导航栏都是一样的(大部分网站都差不多) 变化的区域(跟随链接变化)主要是在内容区 所以可以把导航栏专门提炼出来 在vue中做一个组件 ## 创建导航栏组件 ### 关于组件 组件一般在web\src\components下创建 ![[image-c88bd9fe.png]] 一个知识点:在vue中 给组件命名 必须要有两个字母大写 否则报错 这里我们命名成NavBar.vue 每个vue里的组件都分三个部分 html、js、css html写在template里 js写在script里 css写在style里 style最好加个scoped 使得css生成时会加上随机字符串 从而不会影响到组件以外的部分 ```html ``` tip: 一个搜索样式的网站 比如这里要实现导航栏 直接去里面搜Navbar就有n种选择给你 无需自己设计 ![[image-0cabef9f.png]] ### 导入组件 在App.vue中修改如下: ```html ``` 即可发现成功引入了刚刚的组件 ![[image-74cbe704.png]] 但样式没有成功用上 这里需要用到bootstrap 再在App.vue中引入两行 ```javascript import "bootstrap/dist/css/bootstrap.min.css" import "bootstrap/dist/js/bootstrap" ``` ![[image-d1c9a95c.png]] 发现报错 缺少@popperjs/core依赖 直接去vue ui里面下载该依赖 ![[image-2210fccc.png]] 刷新一下就解决了 ![[image-50d5eddf.png]] 成功做好了标题栏 再稍微改一下就好了 ### 导航实现 大概改了一下 NavBar,vue如下:(button那一块别删 我这里删掉了 后面又加回来了 是缩小页面变更导航栏样式为三杠样式的功能) ```html King Of Bots 对战 对局列表 排行榜 Zwww 我的Bot 退出 ``` App.vue如下: ```html ``` 效果如下: ![[image-f9be65ee.png]] ## 页面实现 接下来就是要实现 输入http://localhost:8080/pk/ 跳转到对战列表 输入http://localhost:8080/record/ 跳转到对战记录列表 输入http://localhost:8080/ranklist/ 跳转到排行榜 输入http://localhost:8080/userBots/ 跳转到用户下myBot界面 先抛开后端不说 后端大概就是做几个函数用注解对应上 前端总要做出几个不同的页面来吧 每个页面一般也是写一个自己单独的组件 要写几个页面呢 pk、record、ranklist、userBots、404(不合法页面自动跳到404) 页面的组件 一般放在view目录下 (放前面说的components也可以) ![[image-8a472661.png]] 然后 每个页面可能又包含很多组件 所以不要单纯一个页面建一个组件 而是建一个文件夹 而每个组件肯定都要有一个主页 由此得到以下层级 ![[image-d17e5a2a.png]] 再把每个组件的三个框架写好 对应描述一下该页面是什么 如: ```html 对战 ``` ### 测试链接/页面对应 vue里面的路径是怎么一回事 我们可以观察到 主页现在就两个东西 一个导航栏 一个router-view ![[image-88eb8122.png]] 路由会自动根据我们的网址来变 而他的变化方式在web\src\router\index.js中定义 先把刚创建的几个组件都引入进去 ```python import PkIndexView from '../views/pk/PkIndexView.vue' import RecordIndexView from '../views/record/RecordIndexView.vue' import RanklistIndexView from '../views/ranklist/RanklistIndexView.vue' import UserBotIndexView from '../views/user/bots/UserBotIndexView.vue' import NotFound from '../views/error/NotFound.vue' ``` 在const routes = [] 中写映射关系 ```javascript const routes = [ { path:"/pk/",//把http://localhost:8080/pk/ name:"pk_index", component:PkIndexView,//映射到PkIndexView组件 }, { path:"/record/", name:"record_index", component:RecordIndexView, }, { path:"/ranklist/", name:"ranklist_index", component:RanklistIndexView, }, { path:"/user/bot/", name:"user_bot_index", component:UserBotIndexView, }, { path:"/404/", name:"404", component:NotFound, }, ] ``` router/index.js如下: ```javascript import { createRouter, createWebHistory } from 'vue-router' import PkIndexView from '../views/pk/PkIndexView.vue' import RecordIndexView from '../views/record/RecordIndexView.vue' import RanklistIndexView from '../views/ranklist/RanklistIndexView.vue' import UserBotIndexView from '../views/user/bots/UserBotIndexView.vue' import NotFound from '../views/error/NotFound.vue' const routes = [ { path:"/pk/",//把http://localhost:8080/pk/ name:"pk_index", component:PkIndexView,//映射到PkIndexView组件 }, { path:"/record/", name:"record_index", component:RecordIndexView, }, { path:"/ranklist/", name:"ranklist_index", component:RanklistIndexView, }, { path:"/user/bot/", name:"user_bot_index", component:UserBotIndexView, }, { path:"/404/", name:"404", component:NotFound, }, ] const router = createRouter({ history: createWebHistory(), routes }) export default router ``` 成功: ![[image-1cb9f84f.png]]![[image-ee36f1a3.png]]![[image-ed088212.png]] …… 然后还有一个问题 当直接输入localhost:8080是什么都没有的 我们需要把他重定向到localhost:8080/pk ```javascript { path:"/", name:"home", redirect:"/pk/", }, ``` 另外 如果输入乱七八糟的地址的话 需要跳转到404界面 直接匹配所有字符就行 因为是从上到下执行 所有到这里匹配任何都直接跳到404 ```javascript { path:"/:catchAll(.*)", redirect:"/404/", }, ``` #### 完整路由: ```javascript import { createRouter, createWebHistory } from 'vue-router' import PkIndexView from '../views/pk/PkIndexView.vue' import RecordIndexView from '../views/record/RecordIndexView.vue' import RanklistIndexView from '../views/ranklist/RanklistIndexView.vue' import UserBotIndexView from '../views/user/bots/UserBotIndexView.vue' import NotFound from '../views/error/NotFound.vue' const routes = [ { path:"/", name:"home", redirect:"/pk/", }, { path:"/pk/",//把http://localhost:8080/pk/ name:"pk_index", component:PkIndexView,//映射到PkIndexView组件 }, { path:"/record/", name:"record_index", component:RecordIndexView, }, { path:"/ranklist/", name:"ranklist_index", component:RanklistIndexView, }, { path:"/user/bot/", name:"user_bot_index", component:UserBotIndexView, }, { path:"/404/", name:"404", component:NotFound, }, { path:"/:catchAll(.*)", redirect:"/404/", }, ] const router = createRouter({ history: createWebHistory(), routes }) export default router ``` ### 全局组件 #### 完善跳转(导航升级) 但这都是自己输入网址才跳转 要实现点击跳转 还需要更改导航栏里的超链接 web\src\components\NavBar.vue 修改为: ```html King Of Bots 对战 对局列表 排行榜 Zwww 我的Bot 退出 ``` 我们还可以发现 每次点击 页面都要进行一遍刷新 体验不是很好 其实可以做到 点击不刷新 直接跳转 这需要我们把导航里的标签全都换成标签 ```html King Of Bots 对战 对局列表 排行榜 Zwww 我的Bot 退出 ``` #### 页面卡片 每个页面其实有个框把主要内容凸显出来会比较合适 在网站里也有有个叫做card的东西 直接里面搜 ![[image-db30b683.png]] 在index 的 template里 把 对战 改成div.container>div.card>div.card-body ```html 对战 ``` ![[image-5ceaa3b5.png]] 可见多了个白框 我们在每页都希望有这样一个白框 所以 也可以把这个东西做成一个组件 类似于导航栏的组件 我们写在components当中 创建ContentField.vue ```html ``` 在这里面的内容会被填充 填充的内容 应该放在标签当中 所以得到 ```html ``` 这个组件就写完了 有了它之后 我们直接在各页面import一下 ```html 对战 ``` 中间的内容就会渲染到这个卡片的组件中去 把其他所有页面都修改成这样 (虽然这里体现意义不大 但极大的提高了可维护性 如果要修改卡片样式 只需要改一个地方就行了 还是很有意义的) 比如这里要把框加一个20px的上边距 只需要修改ContentField.vue ```html ``` ![[image-55d2959f.png]] #### 导航栏跟随高亮 web\src\components\NavBar.vue 高亮由active决定 需要哪块高亮就在哪里加active ![[image-34ff1e0e.png]] 要实现自动高亮这个功能 就得取得当前在哪个页面 显然需要路由配合 在NavBar.vue 的js部分加上以下内容 ```javascript // 引入 useRoute 以访问当前路由信息 import { useRoute } from 'vue-router'; // 引入 computed,用于定义计算属性 import { computed } from 'vue'; export default { // Vue 3 组件的 setup 函数 setup() { // 通过 useRoute 获取当前路由对象 const route = useRoute(); // 创建一个计算属性,返回当前路由的名称 let route_name = computed(() => route.name); // 返回计算属性,使其可以在模板中使用 return { route_name, }; }, }; ``` 怎么用这个route\_name 在标签中 如果属性要用表达式 要在前面加个:(v-bind的缩写) 然后就是三元运算符的逻辑 如果router-name等于该页面的名字 就用有active的样式 否则用普通样式 对战 对应三个也这样改就行了 得到代码: ```javascript King Of Bots 对战 对局列表 排行榜 Zwww 我的Bot 退出 ``` 自此 大概框架就完成了 绝大部分网站都可以套用这个模板 接下来逐一实现各个页面 ### pk页面 这一页主要是实现 地图 以及一个双人版的贪吃蛇小游戏 贪吃蛇之前用c做过一次 原理其实就是 计算蛇在每一帧应该出现的位置 然后把它渲染出来 (每秒画一次 每画一次就把前面那张图片覆盖掉 这样在视觉上就是在移动) 那么在这个动画游戏中 每个游戏物品其实都可以抽象出来一个功能 那就是每秒钟能被刷新60次 我们就可以把它抽象成一个基类 #### GameObject.js 基本刷新逻辑 前面提到图片放在assets里面 我们的js也放在里面 创建以下目录 (记得更改一下的bg引用) ![[image-4a41cb09.png]] 在scripts下创一个游戏基类 GameObject.js ##### 存储游戏对象 为了能够在每秒把所有游戏对象都刷新一遍 首先就得把所有游戏对象都存下来 存在数组里面就可以了 `const GAME_OBJECTS=[];` 基类定义: 需要export出去 其他类引入这个文件时 其实主要是引入这个类 在构造函数时 就把游戏对象加进去 ```java const GAME_OBJECTS=[]; export class GameObject{ constructor(){ GAME_OBJECTS.push(this); } } ``` ##### requestAnimationFrame函数 浏览器一般刷新速度为一秒钟60次 requestAnimationFrame函数会在下一次浏览器刷新前执行 在里面传一个回调函数 即可实现在下一次浏览器渲染之前执行一遍 要让他实现每一帧都执行的话 就把这个函数写成回调(迭代)函数 详细: 1. **浏览器刷新速度为每秒钟60次**:这是基于大多数现代显示器的刷新率(refresh rate)为60Hz,因此浏览器的渲染一般会按照60帧每秒的速率来运行。 2. `requestAnimationFrame`**函数的作用**:`requestAnimationFrame` 会在浏览器下一次重绘(刷新)之前调用你传入的回调函数。这意味着,它能确保动画或其他需要刷新页面的操作与浏览器的刷新速率同步,从而使动画更加平滑。 3. **传入一个回调函数**:`requestAnimationFrame` 会将你传入的回调函数安排在下一次浏览器渲染前执行一遍,从而最大限度地优化性能。 4. **实现每一帧都执行**:如果希望在每一帧都执行某个函数,只需将这个函数本身作为回调函数传入 `requestAnimationFrame`。你可以在回调函数内部再次调用 `requestAnimationFrame` 来形成递归调用(iteration),以确保每一帧都被执行。例如: ```javascript function animationLoop() { // 你的动画逻辑 console.log("每一帧执行一次"); // 递归调用,确保下一帧继续执行 requestAnimationFrame(animationLoop); } // 开始动画循环 requestAnimationFrame(animationLoop); ``` 这样,`animationLoop` 函数会在每次浏览器刷新前被调用一次,实现每秒60次(或刷新率指定的次数)的执行。 所以得到: ```java const GAME_OBJECTS=[]; export class GameObject{ constructor(){ GAME_OBJECTS.push(this); } } const step = () => { requestAnimationFrame(step) } requestAnimationFrame (step) ``` ##### GameObject类 游戏对象应该要执行这么几个函数 创建时的start函数 除第一帧外每帧执行一次的update函数 这帧结束后 被下一帧替代 该帧需要被销毁 实现刷新 destory函数 创建和更新因为当前还没加对象所以暂时留白 但删除现在就可以直接写了 ```javascript destroy(){ for(let i in GAME_OBJECTS){ const obj=GAME_OBJECTS[i]; if(obj === this){ GAME_OBJECTS.splice(i); break; } } } ``` 还有一个可能会用到的操作 在删除之前可能会执行一些回调函数 on\_destory() 在删除之前调用一下就可以实现这个函数的执行 由此得到: ```java const GAME_OBJECTS=[]; export class GameObject{ constructor(){ GAME_OBJECTS.push(this); } start(){//只执行一次 } update(){//每一帧执行一次 除了第一帧外 } on_destory(){//删除之前执行 } destroy(){//删除上一个对象 重新生成新的 实现刷新 this.on_destory(); for(let i in GAME_OBJECTS){ const obj=GAME_OBJECTS[i]; if(obj === this){ GAME_OBJECTS.splice(i); break; } } } } const step = () => { requestAnimationFrame(step) } requestAnimationFrame (step) ``` ##### start/update准备工作 需要记录一下有没有执行过 开一个变量 has\_called\_start 执行过为true否则false 另外 要让物品移动就会涉及到一个速度的概念 一般以秒为单位(一秒移动几个像素)这就涉及到时间间隔 移动的距离就是速度\*时间间隔 使用timedelta变量来存储这一帧执行的时刻距离上一帧执行的时刻的时间间隔 构造函数变为: ```javascript constructor(){ GAME_OBJECTS.push(this); this.timedelta=0; this.has_called_start=false; } ``` 那么start和update该怎么被执行(setp回调函数中) 开一个辅助变量 last\_timestamp 来记录上一次执行的时刻 setp函数传入一个参数(当前的执行时刻) 遍历所有的游戏物品 (js中 循环 用of表示遍历值 用in表示遍历下标) 如果发现该物品没执行过start函数 就执行start 并更改状态 否则就执行update函数 并且计算时间间隔(当前时刻-上一时刻) ```javascript let last_timestamp; const step = timestamp => { for(let obj of GAME_OBJECTS){ if(!obj.has_called_start){ obj.has_called_start=true; obj.start(); }else{ obj.timedelta=timestamp-last_timestamp; obj.update(); } } last_timestamp=timestamp; requestAnimationFrame(step) } ``` ##### 初代代码 ```java // 定义全局游戏对象数组 const GAME_OBJECTS = []; export class GameObject { constructor() { GAME_OBJECTS.push(this); this.timedelta = 0; // 记录时间间隔 this.has_called_start = false; // 标记是否调用过 start } start() { // 只执行一次的初始化逻辑 } update() { // 每一帧执行的逻辑,除了第一帧外 } on_destroy() { // 在对象销毁之前执行的回调 } destroy() { this.on_destroy(); // 执行销毁前的回调 // 删除对象 for (let i = 0; i < GAME_OBJECTS.length; i++) { if (GAME_OBJECTS[i] === this) { GAME_OBJECTS.splice(i, 1); // 删除当前对象,删除一个元素 break; } } } } // 用于记录上一次时间戳 let last_timestamp; // 定义 step 函数 const step = (timestamp) => { // 计算每个对象的更新时间 for (let obj of GAME_OBJECTS) { if (!obj.has_called_start) { obj.has_called_start = true; obj.start(); } else { obj.timedelta = timestamp - last_timestamp; obj.update(); } } // 更新 last_timestamp last_timestamp = timestamp; // 继续下一帧调用 requestAnimationFrame(step); } // 开始执行动画循环 requestAnimationFrame(step); ``` #### 地图 ##### GameMap.js web\src\assets\scripts下建立GameMap.js 首先引入基类 `import {GameObject} from “./GameObject”` 这里import需要加上{} 是因为`export class GameObject` 这里仅为export 如果是export default 就无需括 每一个文件最多只有一个export default 相当于java中的public (default引入不用括号 非default引入要括号) [[01-js-import 和 export 的使用|js-import 和 export 的使用]] 创建GameMap类 继承于基类 构造函数需要传入两个参数 一个是画布(游戏都是在画布标签里画) 另一个是画布的父元素 用于动态修改长宽比 (因为浏览器的框可以调整大小 对应的我们的画布棋盘也要变化) 在这里也可以猜到 写的时候不能用绝对距离 而是用相对位置 这里我们设计成13\*13的地图 每一个格子长度为单位1 那么在构造时存一下每一个格子的绝对距离 未来所有的坐标存的都是相对距离 也就是说 13\*13是绝对的 但每个格子的大小是相对的 另外 肯定需要重载一下开始绘制的start 每帧更新的update 以及渲染函数render 由此得到GameMap.js ```java import { GameObject } from "./GameObject"; export class GameMap extends GameObject{ constructor(ctx,parent){ super(); //先执行基类的构造函数 this.ctx=ctx; this.parent=parent; this.L=0;//L表示一个单位的长度 } start(){ } update(){ this.render(); } render(){//渲染 将当前游戏对象画在地图上 } } ``` ##### 绘制地图区域 做地图的话之前的ContentField组件的白框就没必要了 可以删掉 重新创一个组件 PlayGround 用于设置主要的游戏区域 新建个组件GameMap负责实际的游戏地图显示和绘制 ![[image-8b2a8d1b.png]]![[PixPin_2024-10-24_20-33-09-b142c825.gif]] PlayGround就相当于那个绿色框 它会随页面的变化而不断变化 而我们的地图是在这个绿色块里面的 是一个正方形区域 现在希望能在不断变化的绿块中 始终得到面积最大的正方形 作为地图 为什么游戏地图不直接跟随页面变化而变化 还要额外弄个框? 因为游戏区里可能不光包含地图 还有些计分板(结算页面)等功能 为分别控制 得预留些空间 后续可能还会给他们设置组件 便于管理 回到如何计算正方形大小 有算法题的感觉了 转变问题成 在给定长宽中 求出最大的有rows行cols列的矩形 已知宽w高h rows行 cols列 纵方向上每一个单位的高度最大为 h/rows 横方向上每一个单位的宽度最大为 w/cols 那么小正方形的最大边长就是 min{ h/rows , w/cols } 由此得出内部每个小正方形的边长 ###### **效果及代码** ![[PixPin_2024-10-24_21-03-53-bdf10ea9.gif]] PkIndexView.vue: ```html ``` GameMap.js ```java import { GameObject } from "./GameObject"; export class GameMap extends GameObject{ constructor(ctx,parent){ super(); //先执行基类的构造函数 this.ctx=ctx; this.parent=parent; this.L=0;//L表示一个单位的长度 this.rows=13; this.cols=13; } start(){ } update_size(){ this.L=Math.min(this.parent.clientWidth / this.cols, this.parent.clientHeight / this.rows); this.ctx.canvas.width=this.L*this.cols; this.ctx.canvas.height=this.L*this.rows; } update(){ this.update_size(); this.render(); } render(){//渲染 将当前游戏对象画在地图上 this.ctx.fillStyle='red'; this.ctx.fillRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height); } } ``` PlayGround.vue: ```html ``` GameMap.vue: ```java ``` ##### 绘制地图 ![[image-aed7feb5.png]] 先不看障碍物 只管地图 可以发现草坪是一深一浅间隔分布的 角落是浅色 然后一浅一深间隔分布 可以分成奇格偶格 看它横纵坐标之和是奇数还是偶数 是奇数画深色 偶数画浅色 web\src\assets\scripts\GameMap.js render渲染部分修改成 ```javascript render(){ const color_even = "#aad751",color_odd = "#a2d149"; for(let r=0;r0;i--){ this.cells[i]=JSON.parse(JSON.stringify(this.cells[i-1])); } } update_move(){ // test // //每秒向右移动五格 将蛇头的横坐标加上每帧移动的距离即可 // //然后 距离等于速度*时间 速度有了 时间存在了timedelta中 (当前帧距离上一帧的时间间隔) // //除以1000转换成秒 // this.cells[0].x+=this.speed*this.timedelta/1000; const dx=this.next_cell.x-this.cells[0].x; const dy=this.next_cell.y-this.cells[0].y; const distance=Math.sqrt(dx*dx+dy*dy); if(distance 0;i--){ this.cells[i]=JSON.parse(JSON.stringify(this.cells[i-1])); } } update_move(){ // test // //每秒向右移动五格 将蛇头的横坐标加上每帧移动的距离即可 // //然后 距离等于速度*时间 速度有了 时间存在了timedelta中 (当前帧距离上一帧的时间间隔) // //除以1000转换成秒 // this.cells[0].x+=this.speed*this.timedelta/1000; const dx=this.next_cell.x-this.cells[0].x; const dy=this.next_cell.y-this.cells[0].y; const distance=Math.sqrt(dx*dx+dy*dy); //由于有精度问题 所以应该允许误差存在 当误差小于设置值时 视作在同一个点上(类似于浮点数二分) if(distance 0;i--){ this.cells[i]=JSON.parse(JSON.stringify(this.cells[i-1])); } } update_move(){ // test // //每秒向右移动五格 将蛇头的横坐标加上每帧移动的距离即可 // //然后 距离等于速度*时间 速度有了 时间存在了timedelta中 (当前帧距离上一帧的时间间隔) // //除以1000转换成秒 // this.cells[0].x+=this.speed*this.timedelta/1000; const dx=this.next_cell.x-this.cells[0].x; const dy=this.next_cell.y-this.cells[0].y; const distance=Math.sqrt(dx*dx+dy*dy); //由于有精度问题 所以应该允许误差存在 当误差小于设置值时 视作在同一个点上(类似于浮点数二分) if(distance 0;i--){ this.cells[i]=JSON.parse(JSON.stringify(this.cells[i-1])); } //下一步操作撞了 死亡 变白 if(!this.gamemap.check_valid(this.next_cell)){ this.status="die"; } } update_move(){ // test // //每秒向右移动五格 将蛇头的横坐标加上每帧移动的距离即可 // //然后 距离等于速度*时间 速度有了 时间存在了timedelta中 (当前帧距离上一帧的时间间隔) // //除以1000转换成秒 // this.cells[0].x+=this.speed*this.timedelta/1000; const dx=this.next_cell.x-this.cells[0].x; const dy=this.next_cell.y-this.cells[0].y; const distance=Math.sqrt(dx*dx+dy*dy); //由于有精度问题 所以应该允许误差存在 当误差小于设置值时 视作在同一个点上(类似于浮点数二分) if(distance {//绑定一个keydown事件 //在Snake类中定义一个设置方向的接口 方便维护 if(e.key === 'w') snake0.set_direction(0); else if(e.key === 'd') snake0.set_direction(1); else if(e.key === 's') snake0.set_direction(2); else if(e.key === 'a') snake0.set_direction(3); else if(e.key === 'ArrowUp') snake1.set_direction(0); else if(e.key === 'ArrowRight') snake1.set_direction(1); else if(e.key === 'ArrowDown') snake1.set_direction(2); else if(e.key === 'ArrowLeft') snake1.set_direction(3); }); } start(){ for(let i=0;i<1000;i++){//可以 但最好别写死循环 1000次足够生成连通的了 if(this.create_walls()) break; } this.add_listening_events(); } update_size(){ //墙之间有缝隙 是因为L为浮点数 而绘制时是以整个像素来画的 导致某些像素丢失 //解决方法: 取整 但我觉得有缝隙更好看 所以这里不改 // this.L=parseInt(Math.min(this.parent.clientWidth / this.cols, this.parent.clientHeight / this.rows)); this.L=Math.min(this.parent.clientWidth / this.cols, this.parent.clientHeight / this.rows); this.ctx.canvas.width=this.L*this.cols; this.ctx.canvas.height=this.L*this.rows; } check_ready(){ //两条蛇都处于静止 且都获取了下一步操作的时候 就是准备好了 for(const snake of this.snakes){ if(snake.status !== "idle") return false; if(snake.direction === -1) return false; } return true; } next_step(){//让两条蛇进入下一回合 for(const snake of this.snakes){ snake.next_step(); } } check_valid(cell){//检查目标位置是否合法(没撞到两条蛇的身体和障碍物) for(const wall of this.walls){ if(wall.r === cell.r && wall.c === cell.c) return false; } for(const snake of this.snakes){ let k=snake.cells.length; if(!snake.check_tail_increasing()){//当蛇尾会前进时 蛇尾无需判断 不可能会撞 k--; } for(let i=0;i0;i--){ this.cells[i]=JSON.parse(JSON.stringify(this.cells[i-1])); } //下一步操作撞了 死亡 变白 if(!this.gamemap.check_valid(this.next_cell)){ this.status="die"; } } update_move(){ // test // //每秒向右移动五格 将蛇头的横坐标加上每帧移动的距离即可 // //然后 距离等于速度*时间 速度有了 时间存在了timedelta中 (当前帧距离上一帧的时间间隔) // //除以1000转换成秒 // this.cells[0].x+=this.speed*this.timedelta/1000; const dx=this.next_cell.x-this.cells[0].x; const dy=this.next_cell.y-this.cells[0].y; const distance=Math.sqrt(dx*dx+dy*dy); //由于有精度问题 所以应该允许误差存在 当误差小于设置值时 视作在同一个点上(类似于浮点数二分) if(distance {{ resultMessage }} 再来一局! ``` ##### GameMap.vue 中使用 GameResult 组件 ```java ``` ##### 在 GameMap.js 中添加判断游戏结果的逻辑 ![[image-ce201b59.png]]![[image-ed9e7405.png]] ##### 美化弹窗 ```html {{ resultMessage }} 再来一局! ``` ![[end-5de6b63e.gif]] --- **项目分区导航**: [[03-注解(了解)|注解(了解)]] ⬅️ | 00-创建菜单与游戏界面 | ➡️ [[01-js-import 和 export 的使用|js-import 和 export 的使用]]