引言

泡泡龙游戏的核心玩法依赖于物理碰撞与颜色匹配的算法实现。反弹效果需要模拟泡泡与边界或障碍物的弹性碰撞,确保轨迹符合物理规律;匹配算法则需快速检测相邻同色泡泡,触发消除逻辑。高效的处理方式直接影响游戏流畅度和玩家体验。

以下主要介绍反弹与匹配算法的多种实现思路和代码示例。

一、 反弹物理计算

  1. 几何变换法 Canvas/CSS 对称反射,无需手动计算角度,适用于垂直边界的反弹,属于镜像反射 的简化实现

(1)入射角度
计算鼠标相对与射击器中心的角度

//得到一个从射击器中心指向鼠标位置的角度值(单位:角度)
let iAngle = Math.abs((Math.atan2(ey - oy, ex - ox) * 180) / PI);
iAngle = Math.min(170, Math.max(10, iAngle)); //限制射击角度在 10° ~ 170° 范围内
iAngle = -angleToRadian(iAngle); //将角度转换回弧度,并反向处理
//补充工具函数
const normalizeAngle = (angle: number) => {// 角度可能超出 [0°, 360°),先归一化:return ((angle % 360) + 360) % 360;
}const radianTOAngle = (radian: number) => {return radian * 180 / Math.PI;
}const angleToRadian = (angle: number) => {return angle * Math.PI / 180;
}

(2)反射角度

左右反弹通过x轴方向,左边界反弹法线是x轴正方向,反射角度=-π -入射角度
右边界法线是x轴负方向,反射角度= π -入射角度

前端角度转换成数学角度,前端(CSS/Canvas)的旋转角度和数学计算角度差异(如下图),前端需要顺时针转90,方向从顺时转成逆时针取反,就得到90-前端角度=数学角度

图1-前端角度数学角度对比图

网上还有另外一种思路,垂直反射改变水平移动速度
shooterBubble.dx表示泡泡水平移动速度 shooterBubble.dx = -shooterBubble.dx; 实现水平方向反弹, 镜像反射 的简化方式,适用于垂直边界, 若原方向为 30°,碰到右边界后变为 150°(即对称角度),实际上没有计算角度,只是改变 x 轴方向。

  1. 向量计算法 通用 2D/3D 反射 精确,适用于任意方向

(1)概念

利用向量的点积(dot product)和叉积(cross product)计算角度,适用于任意方向的入射角计算。
点积(内积)返回一个标量(数值),用于计算夹角、投影、相似度(光照、碰撞检测)。
叉积(外积)返回一个向量(在 3D 中),用于计算垂直方向、旋转方向、面积(法线、扭矩、几何判断)。

点积定义为 a⋅b=∣a∣⋅∣b∣⋅cosθ, ∣a∣ 和 ∣b∣ 是向量的模, θ 是两向量之间的夹角

在 2D 和 3D 中,点积的计算公式为:
(2D)a⋅b=ax bx +ay by (2D)
(3D)a⋅b=ax bx +ay by +az bz (3D)

function dotProduct(a, b) {return a.x * b.x + a.y * b.y; // 2D
}const a = { x: 1, y: 0 }; // 向右
const b = { x: 0, y: 1 }; // 向上const dot = dotProduct(a, b); // 0(垂直)
const angle = Math.acos(dot / (Math.hypot(a.x, a.y) * Math.hypot(b.x, b.y))); // 90°

(2)计算入射向量和法线向量:

入射向量 v1 = (x1, y1)
表面法线向量 n = (nx, ny)(垂直于反射面)

(3)计算反射向量(根据反射定律):反射向量=v1−2×(v1⋅n)×n(其中 · 是点积)

(4)计算角度:

用 Math.atan2 计算反射向量与水平轴的夹角:Math.atan2(reflectedY, reflectedX)(这里需要注意在数学计算中使用的是弧度值)

function calculateReflectionAngle(incidentVec, normalVec) {const dot = incidentVec.x * normalVec.x + incidentVec.y * normalVec.y;const reflectedX = incidentVec.x - 2 * dot * normalVec.x;const reflectedY = incidentVec.y - 2 * dot * normalVec.y;return Math.atan2(reflectedY, reflectedX); // 返回弧度
}

复习三角函数
a 表示 “arc”(弧),即通过比值反推角度

函数含义范围场景
Math.sin(x)正弦 (对边/斜边)[-1, 1]周期性运动、旋转计算、波形生成
Math.cos(x)余弦 (邻边/斜边)[-1, 1]周期性运动、旋转计算、波形生成
Math.tan(x)正切 (对边/邻边)(-∞, ∞)斜率计算、视角映射、透视校正
Math.asin(x)反正弦[-π/2, π/2]角度反推、碰撞检测
Math.acos(x)反余弦[0, π]角度反推、碰撞检测
Math.atan(x)反正切[-π/2, π/2]简单斜率计算
Math.atan2(x)带象限的反正切[-π, π]精准角度计算(自动处理象限)

二、 迭代BFS算法

  1. 概念

BFS(Breadth-First Search 广度优先搜索)是一种图遍历算法,它从根节点开始,先访问所有相邻节点,然后再依次访问这些相邻节点的相邻节点,以此类推,逐层扩展。

  • 核心思路
    队列数据结构:使用队列来存储待访问的节点(下面代码中queue)
    层级遍历:按照距离起始节点的层级顺序访问
    避免重复访问:使用标记数组或哈希表记录已访问节点(下面代码中visited)
  1. 获取六边形相邻布局节点

从左上开始按顺时针方向进行遍历,不要忘记进行边界检查

  getNeighborsTiles(newPao: Tile): Tile[] {// 定义6个相邻方向(六边形网格)// 顺序:左上、右上、右、右下、左下、左const isEvenRow = (newPao.x % 2 === 0);const directions = [{ dx: -1, dy: isEvenRow ? -1 : 0, info: '左上' },{ dx: -1, dy: isEvenRow ? 0 : 1, info: '右上' },{ dx: 0, dy: 1, info: '右' },{ dx: 1, dy: isEvenRow ? 0 : 1, info: '右下' },{ dx: 1, dy: isEvenRow ? -1 : 0, info: '左下' },{ dx: 0, dy: -1, info: '左' }];const neighbors = [];// 按顺时针方向递归检查相邻泡泡for (const dir of directions) {const nx = newPao.x + dir.dx;const ny = newPao.y + dir.dy;// 边界检查if (nx < 0 || nx >= this.maxRow ||ny < 0 || ny >= (nx % 2 === 0 ? this.maxCol : this.maxCol - 1)) {continue;}const pao = this.grid.cells?.[nx]?.[ny];// console.log(`${nx}-${ny}-${dir.info}`, pao);if (pao) neighbors.push(pao)}return neighbors}
  1. 查找所有相邻的同色泡泡,返回需要消除的泡泡

查找过程:以当前停靠的泡泡为起点,查看周围所有与它相邻的泡泡,如果发现周围有相同颜色(数字相同)的泡泡,那么就以这个泡泡为起点,继续查看其周围相邻的泡泡…一直重复这个过程,直到周围不再有相邻的泡泡为止。时间和空间复杂度O(N)。

  searchRemovePaos(newPao: Tile): Tile[] {// 记录所有相邻的泡泡,六边形布局以左上角泡泡为开始,顺时针进行查找相邻的泡泡const visited: Set<string> = new Set();//已访问集合,避免重复查找;const queue: Tile[] = [newPao]; //BFS 队列用于广度优先查找;const removePaos: Tile[] = [];// 存放最终要消除的泡泡;// 起始泡泡先加入visited.add(`${newPao.x},${newPao.y}`);removePaos.push(newPao);while (queue.length > 0) {const current = queue.pop();console.log('查找current', current);if (!current) continue;const neighbors = this.getNeighborsTiles(current);for (const neighbor of neighbors) {const key = `${neighbor.x},${neighbor.y}`;if (visited.has(key)) continue; // 已经查过,跳过visited.add(key); // 标记为已访问if (neighbor.color === current.color) {removePaos.push(neighbor); // 同色泡泡加入消除列表queue.push(neighbor);      // 并继续扩散查找}}}return removePaos.length >= 3 ? removePaos : [];}
  1. 悬浮泡泡消除

使用BFS 遍历查找所有与顶部相连的泡泡,再次遍历整个网格,找出所有 未与顶部相连的泡泡(悬空泡泡),并返回它们,时间和空间复杂度O(M × N)

  searchDropPaos() {const visited: Set<string> = new Set();//用于记录已经访问过的泡泡const connectedToTop: Set<string> = new Set();// 存储和顶部相连的泡泡const queue: Tile[] = [];// BFS 队列// 初始化队列,将顶部的泡泡加入for (let y = 0; y < (0 % 2 === 0 ? this.maxCol : this.maxCol - 1); y++) {const topPao = this.grid.cells[0][y];if (topPao) {const key = `${0},${y}`;queue.push(topPao);visited.add(key);connectedToTop.add(key);}}// BFS 遍历查找所有与顶部相连的泡泡:while (queue.length > 0) {const current = queue.shift();if (!current) continue;const neighbors = this.getNeighborsTiles(current);for (const neighbor of neighbors) {const key = `${neighbor.x},${neighbor.y}`;if (visited.has(key)) continue;visited.add(key);connectedToTop.add(key);queue.push(neighbor);}}// 只能从顶部出发,找到所有直接或间接与顶部相连的泡泡。但无法直接标记出 未与顶部相连 的泡泡// 需再次遍历整个网格,找出所有未和顶部相连的泡泡const dropPaos: Tile[] = [];for (let x = 0; x < this.boxRow; x++) {const maxColInRow = x % 2 === 0 ? this.maxCol : this.maxCol - 1;for (let y = 0; y < maxColInRow; y++) {const pao = this.grid.cells?.[x]?.[y];if (pao) {const key = `${x},${y}`;if (!connectedToTop.has(key)) {dropPaos.push(pao);}}}}return dropPaos;}

扩展

另一种图遍历算法 DFS(Depth-First Search 深度优先搜索),它沿着一条路径尽可能深入地搜索,直到无法继续前进,然后回溯并尝试其他路径。

  • 核心思路
    栈数据结构:使用栈(递归调用栈或显式栈)来存储待访问节点
    深度优先:尽可能深地探索一条路径
    回溯机制:当无法继续前进时返回上一个分叉点
  1. 递归DFS(隐式栈)

优点:①数据规模小(如二叉树深度<1000)②代码简洁性优先
缺陷:①栈溢出风险:深度过大时(如1万+层)会触发Maximum call stack size exceeded ②函数调用比循环消耗更多资源

// 递归DFS(邻接链表表示)
function dfsRecursive(graph, node, visited = new Set()) {console.log(node);       // 访问节点visited.add(node);for (const neighbor of graph[node]) {if (!visited.has(neighbor)) {dfsRecursive(graph, neighbor, visited); // 递归调用, 这行是隐式使用调用栈(Call Stack)保存状态 visited}}
}// 使用示例
const graph2 = {'A': ['B', 'C'],'B': ['D'],'C': ['E'],'D': [],'E': []
};
dfsRecursive(graph2, 'A'); // 输出: A → B → D → C → E
  1. 迭代DFS(显式栈)

优点:①避免栈溢出:栈大小由堆内存控制,可处理超深结构 ②性能更好:减少函数调用开销 ③灵活控制:可随时暂停/恢复遍历
缺点:代码可读性低需手动管理状态(循环推荐手动管理stack和visited )

// 迭代DFS(显式栈)显式使用栈结构(数组)代替调用栈
function dfsIterative(graph, start) {const stack = [start];         // 显式栈,模拟调用栈无深度限制(受限于堆内存而非调用栈),通过push/pop操作实现栈操作const visited = new Set();// 减少函数调用开销,可随时暂停/恢复遍历,更灵活while (stack.length > 0) {const node = stack.pop();    // 取栈顶if (visited.has(node)) continue;console.log(node);           // 访问节点visited.add(node); // 通过循环推进// 逆序压栈保证遍历顺序(与递归一致)for (let i = graph[node].length - 1; i >= 0; i--) {stack.push(graph[node][i]);}}
}// 使用相同的graph
dfsIterative(graph2, 'A'); // 输出: A → C → E → B → D
特性BFSDFS
数据结构队列栈(递归或显式)
空间复杂度O(V)(最宽层级)O(V)(最长路径)
最短路径可以找到(无权图)不一定能找到
实现方式通常迭代实现递归或迭代实现
适用场景最短路径、层级关系拓扑排序、连通性检测

总结

泡泡反弹使用Math.atan2(mouseY - SHOOTER_Y, mouseX - SHOOTER_X)计算鼠标相对于炮台的发射角度,简单的垂直方向反射可以直接用 PI 去计算,其他方向反射用向量计算法更准确。泡泡消除查询匹配使用BFS,BFS 适合从顶部逐层向下扩展,天然符合“与顶部连接”的逻辑。更容易控制边界条件:BFS 按层处理,适合六边形网格这种结构较复杂的布局。DFS 不利于全局连通性判断:DFS 更适合路径探索或图的连通性判定,但在这种网格结构中,BFS 更直观、更高效。

参考资料
  • 【泡泡龙游戏】泡泡如何发射,反弹,移动,停靠
  • 【泡泡龙游戏】核心查找匹配算法

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用deepseek满血版

deepseek 访问方式 DeepSeek满血版可通过官方网站或官方应用商店下载安装。确保设备满足最低系统要求&#xff0c;如操作系统版本和硬件配置。 账号注册与登录 访问平台后完成账号注册流程&#xff0c;提供必要信息并验证邮箱或手机号。登录后进入用户中心&#xff0c;查看…

网络管理【Linux/Unix/Windows】命令大全

在跨平台网络运维中&#xff0c;管理员常需快速切换Windows与Linux环境下的命令操作。本文整合了核心网络管理命令的跨平台对照表&#xff0c;涵盖连通性测试、路由追踪、DNS解析、ARP管理、会话监控等高频场景。无论您负责服务器维护、网络排障还是安全审计&#xff0c;此表可…

Gremlin创建schema(包括实体和关系)

1、构建图谱schema&#xff0c;流程包括图创建、实体构建以及关系构建。 创建图时需要指定图库名称以及主键字段。 实体构建时需要指定主键字段&#xff0c;每个属性需要指定数据类型&#xff0c;是否非空以及默认值。关系构建时需要包括关系名称、指向头实体的标签&#xff0c…

[论文阅读]TrustRAG: Enhancing Robustness and Trustworthiness in RAG

TrustRAG: Enhancing Robustness and Trustworthiness in RAG [2501.00879] TrustRAG: Enhancing Robustness and Trustworthiness in Retrieval-Augmented Generation 代码&#xff1a;HuichiZhou/TrustRAG: Code for "TrustRAG: Enhancing Robustness and Trustworthin…

鸿蒙Next仓颉语言开发实战教程:店铺详情页

各位好&#xff0c;幽蓝君又来分享仓颉开发教程了&#xff0c;今天的内容是店铺详情页&#xff1a; 这个页面的内容看似简单&#xff0c;其实有很多小细节需要注意&#xff0c;主要还是让大家熟悉List容器的使用。 整个页面由导航栏和List容器两大部分组成&#xff0c;导航栏我…

FEMFAT许可使用数据分析工具介绍

在高度竞争和快速变化的工程仿真领域&#xff0c;数据驱动的决策变得越来越重要。为了更好地了解FEMFAT许可的使用情况、提高资源利用率、优化工作流程&#xff0c;FEMFAT许可使用数据分析工具应运而生。本文将为您介绍这款强大的工具&#xff0c;助您轻松驾驭FEMFAT许可数据&a…

大模型原理面试题及参考答案

目录 什么是大语言模型(LLM)?它与传统语言模型的本质差异在哪里? 自回归模型(autoregressive)与掩码语言模型(masked LM)的异同是什么?各适合于哪些任务? Transformer 的核心构件——多头自注意力机制如何捕捉长距离依赖? 位置编码(positional encoding)的作用…

Gartner<Reference Architecture Brief: Data Integration>学习心得

数据集成参考架构解析 引言 在当今数字化时代,数据已成为企业最宝贵的资产之一。随着企业规模的不断扩大和业务的日益复杂,数据来源也变得多样化,包括客户关系管理(CRM)、企业资源规划(ERP)、人力资源管理(HR)和市场营销等领域的运营系统。这些系统虽然在其特定功能…

JAVASE:方法

JavaSE 方法详解 一、方法的核心概念 方法&#xff08;Method&#xff09;是一组执行特定任务的语句集合&#xff0c;它将代码逻辑封装为可复用的单元&#xff0c;提高代码的模块化和可维护性。 方法的组成&#xff1a; [修饰符] 返回类型 方法名([参数列表]) {// 方法体[r…

MXNet-cu101 + CUDA 10.1 在 Windows 11 上启用 GPU 的完整指南

一、报错信息 (pytorch) C:\Users\Administrator\Desktop\test>D:/conda/anaconda3/envs/pytorch/python.exe c:/Users/Administrator/Desktop/test/test.py Traceback (most recent call last): File “c:/Users/Administrator/Desktop/test/test.py”, line 1, in import…

Python基础数据类型与运算符全面解析

Python作为一门动态类型语言&#xff0c;拥有丰富的内置数据类型和运算符系统&#xff0c;构成了编程的基础。本文将深入介绍Python核心数据类型的基本概念、特点及使用方法&#xff0c;并系统梳理运算符的分类、优先级和实际应用示例&#xff0c;帮助开发者全面掌握Python的基…

Mysql分区(单服务器应对大数据量方案)

参考资料&#xff1a; 参考视频 参考博客 分区的复杂操作 参考资料 概述&#xff1a; 这里只讲实操&#xff0c;不讲原理&#xff0c;看原理请看参考资料Mysql自5.1后支持分区&#xff0c;在Mysql8之后只有InnoDB支持分区&#xff0c;Mysiam不支持分区本例只是一个简单的说…

[Java恶补day22] 240. 搜索二维矩阵Ⅱ

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17…

基于Master-Slave主从博弈论的储能与能源协调算法matlab仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序 4.系统仿真参数 5.系统原理简介 6.参考文献 7.完整工程文件 1.课题概述 基于Master-Slave主从博弈论的储能与能源协调算法matlab仿真.主从博弈&#xff08;Stackelberg Game&#xff09;是一种具有层级决策结构的博弈模型&am…

vue-print-nb 打印相关问题

一、背景与解决方案 1、ElementUI表格打印通病&#xff0c;均面临边框丢失、宽度超出问题&#xff1a;相关解决代码有注释&#xff1b; 2、大多数情况下不会打印页眉页脚的日期、网址、未配置popTitle显示的undefined&#xff1a;相关解决代码有注释&#xff1b; 3、打印预览页…

Agent应用案例精选,以及主流Agent框架开源项目推荐

一、Agent技术概述 在人工智能领域,Agent(智能体)是指能够感知环境、自主决策并执行动作以实现特定目标的智能系统。随着大语言模型(LLM)的快速发展,基于LLM的Agent系统已成为当前AI研究的热点方向,为复杂任务解决提供了全新范式。 Agent的核心特征 自主性(Autonomy): 能够…

Linux下基础IO

1 文件 这里首先得理解一下文件&#xff0c;文件存放在磁盘中&#xff08;磁盘是永久性存储介质&#xff0c;是一种外设&#xff0c;也是一种输入输出设备&#xff09;&#xff0c;磁盘上的文件的所有操作&#xff0c;都是对外设的输入和输出简称IO&#xff0c;linux下一切皆⽂…

云原生核心技术 (6/12): K8s 从零到一:使用 Minikube/kind 在本地搭建你的第一个 K8s 集群

摘要 本文是一篇保姆级的实践指南&#xff0c;旨在解决学习 Kubernetes (K8s) 时“环境搭建难”的头号痛点。我们将对比分析 Minikube、kind、K3s 和 Docker Desktop Kubernetes 等主流本地 K8s 环境方案的优缺点&#xff0c;帮助你选择最适合自己的工具。随后&#xff0c;文章…

线程运行的现象和相关指令

一.多个线程运行的现象 1.规律 交替执行谁先谁后&#xff0c;不由我们控制 2.举例 Slf4j(topic "c.Test6") public class Test06 {public static void main(String[] args) {//创建并运行线程1new Thread(()->{while (true){log.debug("running");…

Windows网络配置避坑指南

Windows网络配置避坑指南 一、网络配置是什么?防火墙的“信任开关”二、何时需要手动切换网络配置文件?​必需切换的场景高危!绝对禁止选错的两个场景三、3种切换指南(Win10/11通用)方法1:图形化操作(推荐小白)​方法2:用PowerShell强制切换方法3:注册表底层修改(应…