旋转矩形碰撞检测求助:围绕角色旋转的剑与敌人碰撞实现
旋转矩形碰撞检测实现方案(基于SAT)
嘿,我完全懂这种头疼的感觉——你已经把非旋转矩形的碰撞检测玩明白了,结果碰到旋转的剑和敌人的碰撞,一下子就卡壳了对吧?SAT(分离轴定理)听起来确实有点唬人,但拆解开一步步实现,其实没那么难,而且它刚好是处理这类凸多边形碰撞的最优方案,完美适配你的旋转剑场景。
核心思路:分离轴定理(SAT)
先给你简单捋清楚SAT的核心:对于两个凸多边形,如果能找到任意一条轴,让两个多边形在这条轴上的投影完全不重叠,那它们肯定没碰撞;反过来,如果所有可能的分离轴上的投影都重叠,那碰撞就发生了。
对于矩形来说,我们只需要检测每个矩形的两条边的法线方向作为分离轴就够了——不用检测所有方向,这直接把计算量砍到了最低。
第一步:先搞定基础向量操作
SAT全靠向量计算撑着,所以先封装几个常用的向量工具函数,后面会反复用到:
// 向量点积计算 function dot(v1, v2) { return v1.x * v2.x + v1.y * v2.y; } // 计算向量长度的平方(避免开根号,提升性能) function lengthSquared(v) { return v.x * v.x + v.y * v.y; } // 将向量转为单位向量(归一化) function normalize(v) { const len = Math.sqrt(lengthSquared(v)); return { x: v.x / len, y: v.y / len }; } // 获取旋转矩形的两条分离轴(边的法线) function getRectangleAxes(rect) { // rect结构:{ x: 中心x, y: 中心y, width: 宽, height: 高, angle: 旋转角度(度数) } const rad = (rect.angle * Math.PI) / 180; // 计算矩形两条边的方向向量,再取垂直的法线 const axis1 = { x: Math.cos(rad), y: Math.sin(rad) }; const axis2 = { x: -Math.sin(rad), y: Math.cos(rad) }; // 返回归一化后的两条轴 return [normalize(axis1), normalize(axis2)]; }
第二步:实现矩形到轴的投影函数
我们需要把矩形的所有顶点投影到指定轴上,然后找到投影的最小和最大值——这是判断重叠的关键:
function projectRectangle(rect, axis) { const rad = (rect.angle * Math.PI) / 180; const halfW = rect.width / 2; const halfH = rect.height / 2; // 计算旋转矩形的四个顶点坐标 const vertices = [ { x: rect.x + halfW * Math.cos(rad) - halfH * Math.sin(rad), y: rect.y + halfW * Math.sin(rad) + halfH * Math.cos(rad) }, { x: rect.x - halfW * Math.cos(rad) - halfH * Math.sin(rad), y: rect.y - halfW * Math.sin(rad) + halfH * Math.cos(rad) }, { x: rect.x - halfW * Math.cos(rad) + halfH * Math.sin(rad), y: rect.y - halfW * Math.sin(rad) - halfH * Math.cos(rad) }, { x: rect.x + halfW * Math.cos(rad) + halfH * Math.sin(rad), y: rect.y + halfW * Math.sin(rad) - halfH * Math.cos(rad) } ]; // 计算所有顶点在轴上的投影值,找出最小和最大值 let min = dot(vertices[0], axis); let max = min; for (let i = 1; i < 4; i++) { const proj = dot(vertices[i], axis); if (proj < min) min = proj; if (proj > max) max = proj; } return { min, max }; }
第三步:实现完整的SAT碰撞检测函数
把前面的工具函数组合起来,检查两个旋转矩形是否碰撞:
function checkRotatedRectCollision(rectA, rectB) { // 获取两个矩形的所有分离轴(每个矩形两条,共四条) const axes = [...getRectangleAxes(rectA), ...getRectangleAxes(rectB)]; // 遍历每条轴,检查投影是否重叠 for (const axis of axes) { const projA = projectRectangle(rectA, axis); const projB = projectRectangle(rectB, axis); // 如果任意一条轴上投影完全不重叠,说明没有碰撞 if (projA.max < projB.min || projB.max < projA.min) { return false; } } // 所有轴的投影都重叠,说明碰撞发生了 return true; }
第四步:结合你的场景使用
比如你的旋转剑是一个细长矩形,敌人是普通矩形,你可以这样调用:
// 定义旋转剑的矩形(中心和角色重合,角度随帧更新) const sword = { x: player.x, y: player.y, width: 12, height: 90, angle: currentSwordRotation // 比如每帧增加3度,实现旋转效果 }; // 定义敌人的矩形(如果敌人不旋转,angle设为0即可) const enemy = { x: enemyPosition.x, y: enemyPosition.y, width: 36, height: 36, angle: 0 }; // 检测碰撞,触发击杀逻辑 if (checkRotatedRectCollision(sword, enemy)) { handleEnemyKill(enemy); }
额外优化小技巧
- AABB预检测:先计算两个旋转矩形的轴对齐包围盒(把旋转矩形包起来的最小轴对齐矩形),如果这个AABB都不碰撞,直接跳过SAT检测,能大幅提升性能。
- 碰撞响应扩展:如果需要知道碰撞的深度(比如把敌人推开),可以在遍历轴的时候记录最小的重叠量和对应的轴,用这个轴来计算碰撞方向和深度。
这样一步步走下来,SAT是不是没你想象的那么复杂?这个方案刚好适配你的旋转剑击杀敌人的需求,赶紧试试吧!
内容的提问来源于stack exchange,提问作者Elie Jamous




