3. 二维碰撞检测
碰撞检测,说白了就是判断两个物体有没有「碰在一起」。做机器人路径规划也好,做游戏物理引擎也罢,这步都是地基。我刚开始写代码那会儿,觉得碰撞检测不就是比大小嘛,结果一跑起来各种漏检、误检,调试到怀疑人生。
二维碰撞检测的核心就三件事:几何关系、分离轴、边界条件。今天咱们把最常用的三种情况讲透——点与矩形、圆与圆、矩形与矩形。你想想看,这几种搞明白了,后面三维的、多边形的,思路都是类似的。
3.1 点与矩形的碰撞检测
这是最简单的碰撞检测,没有之一。判断一个点是否在矩形内部,只需要比较坐标范围。
假设矩形用 左上角 (rx, ry) 和 宽高 (rw, rh) 表示,点坐标为 (px, py)。那么碰撞条件就是:
// 点与轴对齐矩形(AABB)碰撞检测
bool pointRectCollision(float px, float py,
float rx, float ry,
float rw, float rh) {
// 点在矩形左右边界之间
bool inX = (px >= rx) && (px <= rx + rw);
// 点在矩形上下边界之间
bool inY = (py >= ry) && (py <= ry + rh);
return inX && inY;
}
>= 和 <= 还是 > 和 <?这取决于你的坐标系和「碰撞」的定义。我个人习惯用闭区间,因为当点刚好在边界上时,通常也算碰撞。但如果你做的是物理模拟,开区间可以避免「粘边」的抖动问题。
我在项目中遇到过一个问题:用鼠标点击选择屏幕上的物体时,如果矩形有旋转,上面的代码就不灵了。这时候需要把点转换到矩形的局部坐标系再做判断。不过那是进阶话题,咱们先把基础打牢。
3.2 圆与圆的碰撞检测
圆与圆的碰撞检测,核心就一句话:两圆心距离小于半径之和。
但这里有个坑——你千万别直接算距离再开平方。开平方是昂贵的操作,在实时系统中能省则省。正确的做法是:比较距离的平方与半径和的平方。
// 圆与圆碰撞检测(优化版)
bool circleCircleCollision(float x1, float y1, float r1,
float x2, float y2, float r2) {
// 计算圆心距离的平方
float dx = x2 - x1;
float dy = y2 - y1;
float distSq = dx * dx + dy * dy;
// 计算半径和的平方
float radSum = r1 + r2;
float radSumSq = radSum * radSum;
// 比较(避免开平方)
return distSq <= radSumSq;
}
sqrt() 做碰撞检测,在嵌入式机器人上帧率直接掉了一半。后来改成平方比较,性能瞬间恢复。记住:能平方就不开方,这是碰撞检测的黄金法则。
你想想看,如果两个圆完全重合,距离为0,肯定小于半径和,所以也算碰撞。这在某些场景下需要特殊处理——比如两个机器人完全重叠时,你可能需要把它们「弹开」而不是继续检测。
3.3 矩形与矩形的碰撞检测
矩形与矩形的碰撞检测,分两种情况:轴对齐矩形(AABB) 和 有旋转的矩形(OBB)。咱们先讲最常用的 AABB。
3.3.1 AABB 碰撞检测
AABB 的碰撞检测思路很直观:两个矩形在 X 轴和 Y 轴上的投影都重叠,才算碰撞。说白了就是「两个方向都重叠」。
// AABB 碰撞检测
bool aabbCollision(float ax, float ay, float aw, float ah,
float bx, float by, float bw, float bh) {
// X 轴投影是否重叠
bool overlapX = (ax < bx + bw) && (bx < ax + aw);
// Y 轴投影是否重叠
bool overlapY = (ay < by + bh) && (by < ay + ah);
return overlapX && overlapY;
}
这个算法的本质是分离轴定理(SAT)的简化版。对于 AABB,两个矩形的分离轴就是 X 轴和 Y 轴。只要在任意一个轴上投影不重叠,两个矩形就不相交。
3.3.2 OBB 碰撞检测(带旋转的矩形)
如果矩形有旋转角度,事情就复杂了。这时候需要用分离轴定理(SAT)。对于两个 OBB,最多需要检测 4 条分离轴(每个矩形的两条边法线)。
嗯,这里要注意:SAT 的核心是「如果存在一条轴,两个多边形在该轴上的投影不重叠,则它们不相交」。对于矩形,我们只需要检测每条边的法线方向。
// OBB 碰撞检测(简化版,仅展示核心逻辑)
bool obbCollision(OBB& a, OBB& b) {
// 获取两个矩形的四个轴(每个矩形两条边的法线)
Vector2 axes[4] = {
a.getAxisX(), a.getAxisY(),
b.getAxisX(), b.getAxisY()
};
for (int i = 0; i < 4; i++) {
// 计算两个矩形在当前轴上的投影
float projA = a.project(axes[i]);
float projB = b.project(axes[i]);
// 如果投影不重叠,则不相交
if (!overlap(projA, projB)) {
return false; // 找到分离轴,不相交
}
}
return true; // 所有轴都重叠,相交
}
3.4 三种算法的对比与选型
| 算法 | 计算量 | 适用场景 | 精度 | 旋转支持 |
|---|---|---|---|---|
| 点与矩形 | 4 次比较 | 鼠标拾取、传感器点云 | 精确 | 否(需转换) |
| 圆与圆 | 3 次乘法 + 1 次比较 | 圆形机器人、球状障碍物 | 精确 | 天然支持 |
| AABB | 4 次比较 | 矩形障碍物、粗筛阶段 | 精确(无旋转) | 否 |
| OBB (SAT) | 8-16 次投影计算 | 旋转矩形、精确碰撞 | 精确 | 是 |
3.5 实战中的注意事项
- 浮点数精度:比较时不要用
==,用<= epsilon。我习惯设epsilon = 1e-6。 - 坐标系统一:确保所有物体在同一个坐标系下做检测。我踩过坑——把世界坐标和局部坐标混着用,结果碰撞检测全乱套。
- 性能优化:如果检测对象超过 100 对,考虑空间分区(四叉树、网格)。别裸奔做 O(n²) 检测。
- 边界情况:两个物体刚好相切算不算碰撞?这取决于你的业务逻辑。在路径规划中,相切通常算「危险」,需要避让。