SpringBoot学习笔记-创建菜单与游戏页面(下)
笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接:AcWing SpringBoot 框架课。
CONTENTS
- 1. 地图优化改进
- 2. 绘制玩家的起始位置
- 3. 实现玩家移动
- 4. 优化蛇的身体效果
- 5. 碰撞检测实现
本节实现两名玩家即两条蛇的绘制与人工操作移动功能。
1. 地图优化改进
之前我们设计的地图尺寸是13×13,两名玩家的起点横纵坐标之和均为偶数,因此可能在同一时刻走到同一个格子上,为了避免这种情况,可以将地图改为13×14的大小(即将 GameMap.js
中的 this.cols
改为14),这样两名玩家就不会在同一时刻走到同一个格子上。这样修改完之后就不能用轴对称了,需要改为中心对称:
// 添加地图内部的随机障碍物,需要有对称性因此枚举一半即可,另一半对称生成
for (let i = 0; i < this.inner_walls_count / 2; i++) {for (let j = 0; j < 10000; j++) {let r = parseInt(Math.random() * this.rows);let c = parseInt(Math.random() * this.cols);if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue;if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) continue; // 判断是否覆盖到出生地g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;break;}
}
2. 绘制玩家的起始位置
刚开始玩家占一个格子,我们可以规定一下前十步的每一步将蛇的长度加一,之后改为每三步长度加一。每条蛇其实就是一堆格子的序列,我们可以将一个格子定义成一个 Cell
对象,在 scripts
目录下创建 Cell.js
记录格子的坐标。
我们在每个格子中绘制的是一个圆,若格子的左上角坐标为 (x, y)
则圆的圆心坐标应该是 (x + 0.5, y + 0.5)
,Cell.js
如下:
export class Cell {constructor(r, c) {this.r = r;this.c = c;this.x = c + 0.5;this.y = r + 0.5;}
}
此外每条蛇也可以定义成一个对象 Snake.js
:
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";export class Snake extends AcGameObject {constructor(info, gamemap) {super();this.id = info.id; // 每条蛇有唯一的id进行区分this.color = info.color; // 颜色this.gamemap = gamemap;this.cells = [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头}start() {}update() {this.render();}render() {const L = this.gamemap.L;const ctx = this.gamemap.ctx;ctx.fillStyle = this.color;for (const cell of this.cells) {ctx.beginPath();ctx.arc(cell.x * L, cell.y * L, L / 2, 0, Math.PI * 2); // 半径为半个单元格ctx.fill();}}
}
然后我们在 GameMap.js
中创建两条蛇:
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";export class GameMap extends AcGameObject {constructor(ctx, parent) { // ctx表示画布,parent表示画布的父元素super();this.ctx = ctx;this.parent = parent;this.L = 0; // 一个单位的绝对长度this.rows = 13; // 地图的行数this.cols = 14; // 地图的列数this.inner_walls_count = 20; // 地图内部的随机障碍物数量,需要是偶数this.walls = []; // 所有的障碍物this.snakes = [new Snake({ id: 0, color: "#4876EC", r: this.rows - 2, c: 1 }, this),new Snake({ id: 1, color: "#F94848", r: 1, c: this.cols - 2 }, this),];}check_connectivity(g, sx, sy, tx, ty) { // 用flood fill算法判断两名玩家是否连通...}create_walls() {...}start() {...}update_size() { // 每一帧更新地图大小...}update() {...}render() {...}
}
3. 实现玩家移动
为了实现蛇移动的连续性,我们不对每个格子进行更新,只更新头部和尾部,头部创建一个新的点往前动,尾部直接往前动。首先在 Snake
对象中设置一些移动的属性:
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";export class Snake extends AcGameObject {constructor(info, gamemap) {super();this.id = info.id; // 每条蛇有唯一的id进行区分this.color = info.color; // 颜色this.gamemap = gamemap;this.cells = [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头this.speed = 2; // 蛇每秒走2个格子this.direction = -1; // 下一步移动的指令,-1表示没有指令,0、1、2、3分别表示上、右、下、左this.status = "idle"; // 蛇的状态,idle表示静止,move表示正在移动,die表示死亡this.next_cell = null; // 下一步的目标位置this.step = 0; // 回合数this.dr = [-1, 0, 1, 0];this.dc = [0, 1, 0, -1];}start() {}next_step() { // 将蛇的状态变为走下一步const d = this.direction;this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);this.direction = -1; // 复原this.status = "move";this.step++;}set_direction(d) { // 由于未来不一定只会从键盘获取输入,因此实现一个接口修改directionthis.direction = d;}update_move() {}update() {if (this.status === "move") {this.update_move();}this.render();}render() {...}
}
由于游戏是回合制的,因此移动的判定条件应该是获取到了两名玩家的指示后才能移动一次,且该指令既可以由键盘输入也可以由 AI 代码输入,判定两条蛇是否准备好执行下一步不能各自判断,需要由上层也就是 GameMap
判定,判定条件是两条蛇都处于静止状态且都已经获取到了下一步指令:
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";export class GameMap extends AcGameObject {constructor(ctx, parent) { // ctx表示画布,parent表示画布的父元素...}check_connectivity(g, sx, sy, tx, ty) { // 用flood fill算法判断两名玩家是否连通...}create_walls() {...}start() {...}update_size() { // 每一帧更新地图大小...}check_ready() { // 判断两条蛇是否都准备好下一回合了for (const snake of this.snakes) {if (snake.status !== 'idle' || snake.direction === -1) return false;}return true;}next_step() { // 让两条蛇进入下一回合for (const snake of this.snakes) {snake.next_step();}}update() {this.update_size();if (this.check_ready()) {this.next_step();}this.render();}render() {...}
}
现在我们只能从前端获得用户的操作,即获取用户的键盘输入。为了能够让 Canvas 获取键盘输入,需要添加一个 tabindex
属性,在 GameMap.vue
中进行修改:
<template><div ref="parent" class="gamemap"><canvas ref="canvas" tabindex="0"></canvas></div>
</template><script>
...
</script><style scoped>
...
</style>
这样我们就能够在 GameMap.js
中绑定键盘的监听事件:
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";export class GameMap extends AcGameObject {...add_listening_events() {this.ctx.canvas.focus(); // 使Canvas聚焦const [snake0, snake1] = this.snakes;this.ctx.canvas.addEventListener("keydown", e => {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 < 10000; i++) { // 暴力枚举直至生成合法的地图if (this.create_walls())break;}this.add_listening_events();}...
}
现在我们即可在 Snake.js
中实现蛇的移动:
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";export class Snake extends AcGameObject {constructor(info, gamemap) {super();this.id = info.id; // 每条蛇有唯一的id进行区分this.color = info.color; // 颜色this.gamemap = gamemap;this.cells = [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头this.speed = 2; // 蛇每秒走2个格子this.direction = -1; // 下一步移动的指令,-1表示没有指令,0、1、2、3分别表示上、右、下、左this.status = "idle"; // 蛇的状态,idle表示静止,move表示正在移动,die表示死亡this.next_cell = null; // 下一步的目标位置this.step = 0; // 回合数this.dr = [-1, 0, 1, 0];this.dc = [0, 1, 0, -1];this.eps = 0.01; // 误差}start() {}next_step() { // 将蛇的状态变为走下一步const d = this.direction;this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);this.direction = -1; // 复原this.status = "move";this.step++;const k = this.cells.length;for (let i = k; i > 0; i--) { // 将所有节点向后移动一位,因为要在头节点前面插入新的头节点this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); // 注意要深层复制一份,还有一个细节是JS的数组越界不会出错}}set_direction(d) { // 由于未来不一定只会从键盘获取输入,因此实现一个接口修改directionthis.direction = d;}update_move() { // 将头节点cells[0]向目标节点next_cell移动const dx = this.next_cell.x - this.cells[0].x; // 在x方向上与目的地的偏移量const dy = this.next_cell.y - this.cells[0].y; // 在y方向上与目的地的偏移量const distance = Math.sqrt(dx * dx + dy * dy); // 与目的地的距离if (distance < this.eps) { // 已经走到目标点this.status = "idle"; // 状态变为静止this.cells[0] = this.next_cell; // 将头部更新为目标点this.next_cell = null;} else {const move_length = this.speed * this.timedelta / 1000; // 每一帧移动的距离const cos_theta = dx / distance; // cos值const sin_theta = dy / distance; // sin值this.cells[0].x += move_length * cos_theta;this.cells[0].y += move_length * sin_theta;}}update() {if (this.status === "move") { // 只有移动状态才执行update_move函数this.update_move();}this.render();}render() {...}
}
接着我们还需要实现蛇尾的移动,如果蛇的长度增加了一个单位,那么尾部不用动即可,否则尾部需要向前一个节点移动,且当移动完成后需要将尾部节点对象删去:
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";export class Snake extends AcGameObject {...check_tail_increasing() { // 检测当前回合蛇的长度是否增加if (this.step <= 7) return true; // 前7回合每一回合长度都增加if (this.step % 3 === 1) return true; // 之后每3回合增加一次长度return false;}update_move() { // 将头节点cells[0]向目标节点next_cell移动const dx = this.next_cell.x - this.cells[0].x; // 在x方向上与目的地的偏移量const dy = this.next_cell.y - this.cells[0].y; // 在y方向上与目的地的偏移量const distance = Math.sqrt(dx * dx + dy * dy); // 与目的地的距离if (distance < this.eps) { // 已经走到目标点this.status = "idle"; // 状态变为静止this.cells[0] = this.next_cell; // 将头部更新为目标点this.next_cell = null;if (!this.check_tail_increasing()) { // 尾部没有变长则移动完成后要删去尾部this.cells.pop();}} else {const move_length = this.speed * this.timedelta / 1000; // 每一帧移动的距离const cos_theta = dx / distance; // cos值const sin_theta = dy / distance; // sin值this.cells[0].x += move_length * cos_theta;this.cells[0].y += move_length * sin_theta;if (!this.check_tail_increasing()) {const k = this.cells.length;const tail = this.cells[k - 1], tail_target = this.cells[k - 2];const tail_dx = tail_target.x - tail.x;const tail_dy = tail_target.y - tail.y;tail.x += move_length * tail_dx / distance; // 此处就不分开计算cos和sin了tail.y += move_length * tail_dy / distance;}}}update() {if (this.status === "move") { // 只有移动状态才执行update_move函数this.update_move();}this.render();}...
}
4. 优化蛇的身体效果
现在我们蛇的身体还是分开的若干个圆球,没有连续感。我们可以在两个相邻的圆球中间绘制一个矩形覆盖一遍即可。然后我们这边再做个小优化,将蛇的半径缩小一点,不然贴在一起时就会融合在一起不好看:
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";export class Snake extends AcGameObject {constructor(info, gamemap) {...this.radius = 0.4; // 蛇中每个节点的半径...}...render() {const L = this.gamemap.L;const ctx = this.gamemap.ctx;ctx.fillStyle = this.color;for (const cell of this.cells) {ctx.beginPath();ctx.arc(cell.x * L, cell.y * L, L * this.radius, 0, Math.PI * 2);ctx.fill();}// 将相邻的两个球连在一起for (let i = 1; i < this.cells.length; i++) {const a = this.cells[i - 1], b = this.cells[i];if (Math.abs(a.x - b.x) < this.eps && Math.abs(a.y - b.y) < this.eps)continue;if (Math.abs(a.x - b.x) < this.eps) { // 上下排列,即x相同,左上角的点的y值为两者的最小值,因为越往上y越小ctx.fillRect((a.x - this.radius) * L, Math.min(a.y, b.y) * L, 2 * this.radius * L, Math.abs(a.y - b.y) * L);} else { // 左右排列,画法同理ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - this.radius) * L, Math.abs(a.x - b.x) * L, 2 * this.radius * L);}}}
}
5. 碰撞检测实现
相关文章:
SpringBoot学习笔记-创建菜单与游戏页面(下)
笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接:AcWing SpringBoot 框架课。 CONTENTS 1. 地图优化改进2. 绘制玩家的起始位置3. 实现玩家移动4. 优化蛇的身体效果5. 碰撞检测实现 本节实现两名玩家即两条蛇的绘制与人工操作移动功能。 1. 地…...
STM32一
0.前言 在B站经常看见有人用stm32做出了有趣的电子小玩艺儿,感到很羡慕,于是想了解一下。 1.什么是stm32 STM32 是一系列由STMicroelectronics(意法半导体)公司设计和制造的32位ARM Cortex-M微控制器。这一系列的微控制器广泛用…...
GPT-4 Turbo Assistants API
Assistants API Assistants API 允许您在自己的应用程序中构建 AI 助手。助手有指令,可以利用模型、工具和知识来响应用户查询。Assistants API 目前支持三种类型的工具:代码解释器、检索和函数调用。未来,我们计划发布更多 OpenAI 构建的工…...
day08_回顾与课程概括
回顾与课程概括 一、上节课复习 一、上节课复习 1、osi七层与数据传输 2、socketsocket是对传输层以下的封装ipport标识唯一一个基于网络通讯的软件3、tcp与udptcp:因为在通信之前必须建立双向连接,通常都是客户端主动连接服务端的,所以必须…...

iptables、netfilter、firewalld、ufd简单介绍
参考:...

Python基础入门例程53-NP53 前10个偶数(循环语句)
最近的博文: Python基础入门例程52-NP52 累加数与平均值(循环语句)-CSDN博客 Python基础入门例程51-NP51 列表的最大与最小(循环语句)-CSDN博客 Python基础入门例程50-NP50 程序员节(循环语句)-CSDN博客 目录 最近的博文: 描…...

v-bind和v-model
目录 前言 v-bind 作用 语法格式 编译原理 简写 v-model 作用 使用方法 v-bind和v-model的区别和联系 前言 本文我们来了解一下模板语法之指令语法中的v-bind和v-model v-bind 作用 v-bind可以让html标签的某个属性的值产生动态的效果 语法格式 <html标签 v-bin…...

Adobe premiere裁剪视频尺寸并转为GIF格式
第 1 步:裁剪视频 修改序列设置以适应裁剪之后的图像区域;序列中的编辑模式不能使用默认的,这里使用的是“ProRes RAW” 第 2 步:设置背景色 需要设置“颜色遮罩”的大小和颜色,颜色遮罩放在下面。 第 3 步࿱…...
关于react输入框回显问题
绑定表单元素的值到组件状态中。例如,对于一个文本框,可以使用onChange事件将用户输入的值绑定到组件状态中。 创建一个处理表单提交的函数。这个函数通常会使用组件状态中的值来更新页面上的数据。 在handleSubmit函数中,防止默认表单提交…...

案例续集留言板
前端没有保存数据的功能,后端把数据保存下来(内存,数据库等等......) 前端代码如下 : <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initia…...

72 内网安全-域横向CSMSF联动及应急响应初识
目录 演示案例:MSF&CobaltStrike联动ShellWEB攻击应急响应朔源-后门,日志WIN系统攻击应急响应朔源-后门,日志,流量临时给大家看看学的好的怎么干对应CTF比赛 涉及资源 权限维持留到后面在补充,先把后面的知识点给大家讲起来,因为权限维持它是我们前期…...

Leetcode—20.有效的括号【简单】
2023每日刷题(二十七) Leetcode—20.有效的括号 C实现代码 class Solution { public:bool isValid(string s) {stack<char> arr;int len s.size();if(len 1) {return false;}for(int i 0; i < len; i) {if(s[i] ( || s[i] [ || s[i] {)…...

Leetcode—剑指OfferII LCR 019.验证回文串II【简单】
2023每日刷题(二十七) Leetcode—剑指OfferII LCR 019.验证回文串II 实现代码 class Solution { public:bool judgeFunc(string s, int left, int right) {while(left < right) {if(s[left] ! s[right]) {return false;}left;right--;}return true;…...

Mac电脑配置Flutter开发环境
1.进入官网下载页: Flutter SDK releases | Flutter 可以看到有 Windows、macOS、Linux三种系统的下载包 选择macOS,然后点击下载 Stable channel(稳定版)中的最新版本,下载完成后可以移动到资源库Library中。 2.下载…...
QTableView如何清空数据保留表头
QTableView如何清空数据保留表头 调用QAbstractItemModel中的removeRows或者removeColumns方法。 方法原型 bool removeRows(int column, int count, const QModelIndex &parent QModelIndex())在支持此功能的模型上,从模型中删除从父级父级下给定行开始的计…...

[工业自动化-17]:西门子S7-15xxx编程 - 软件编程 - PLC编程语言以及与嵌入式编程的比较
目录 一、博图编程语言 1.1 概述 1.2 三种编程语言之间的关系 二、PLC与嵌入式系统的类比 三、PLC编程与嵌入式系统编程的比较 3.1 不同点 3.2 相同点 3.3 PLC是一种专门用于工业控制系统的嵌入式系统 一、博图编程语言 1.1 概述 西门子(Siemens࿰…...

云原生微服务架构及实现技术
云原生是一种技术理念和架构方法,它充分利用云计算的优势,将应用程序和基础设施进行优化,以适应云环境的特性。云原生的设计原则主要包括弹性、韧性、安全性、可观测性、灰度等,旨在让企业在云环境中实现轻量、敏捷、高度自动化的…...
Uniapp语言切换动态修改Js文件
前言 续接上面两篇文章,第一篇文章是uniapp实现多语言切换,第二篇文章是i8n在js中的使用,由于我的菜单是在js文件中,所以我切换的时候除了菜单不实现效果,别的页面都可以实现,本篇文章主要是针对于怎么动态…...

GetSimple CMS忘记密码
GetSimple CMS是一个超简单的 CMS,适合建立个人网站等只需要极少数页面的网站。在站上百科上,是这么说的: GetSimple是一款基于XML存储数据的开源内容管理系统,且易于安装和定制,无需MySQL支持。提供撤销保护和备份功能…...

数据分析面试题1
1.右表为一组数据,尝试进行简单分析,并给出结论(使用公式和图表辅助) ①理解数据 userid:用户id神兽印记消耗数量 ②数据清洗 冻结首行,将列标题的英文字段转换成汉字字段检查是否有重复项:…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...

【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...

面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...