当前位置: 首页 > news >正文

【javascript】泡泡龙游戏中反弹和查找匹配算法

引言

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

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

一、 反弹物理计算

  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 更直观、更高效。

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

相关文章:

  • 第十三章 RTC 实时时钟
  • 走迷宫 II
  • NIFI的处理器:ConsumeMQTT 2.4.0
  • Java异步编程之消息队列疑难问题拆解
  • 3.1 数据链路层的功能
  • (LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
  • NLP学习路线图(三十七): 问答系统
  • CppCon 2015 学习:The dangers of C-style casts
  • S1240核心的连接关系和工作流程
  • 【动手学深度学习】3.2. 线性回归的从零开始实现
  • idea中黄色感叹号打开
  • 纯血Harmony NETX 5 打造趣味五子棋:(附源文件)
  • ArcGIS土地利用数据制备、分析及基于FLUS模型土地利用预测技术应用
  • 1.4 超级终端
  • gbase8s之message log rotate
  • 路径规划算法概论:从理论到实践
  • Python 基础语法(1)【 适合0基础 】
  • C# StringBuilder代码中预分配容量的作用
  • Java免费获取汇率工具实现
  • 【计算机组成原理 第5版】白、戴编著 第四章 指令系统 课后题总结
  • 联锁酒店网站建设需求分析/清远网站seo
  • 中铁韩城建设公司网站/网络市场调研
  • 如何做网站维护 找关键词/什么是市场营销
  • 烟台商机互联做网站吗/刘连康seo培训哪家强
  • 做3d模型网站赚钱么/什么软件引流客源最快
  • wap网站为什么没有了/网站点击量与排名