把章节变成可单步观察的过程
分类:算法 / 数据结构 / 游戏逻辑常用模式
先说结论
碰撞检测分阶段,是因为“快速排除绝大多数不可能碰撞的对象”比“对所有对象都做精确几何计算”更划算。Broad Phase 产生候选对,Narrow Phase 再用 AABB、OBB、SAT 或其他形状算法确认。
1000 个物体为什么不能全配对
1000 个物体的无序两两组合:
1000 * 999 / 2 = 499,500 对
Broad Phase 假设缩到 120 对:
Narrow Phase 只需精判 120 次
120 是示意值,不是固定比例。若所有物体都堆在一个小区域,Broad Phase 也会产生大量候选;它保证的是不漏掉可能碰撞对,而不是保证候选永远很少。
碰撞检测为什么要分阶段
物理碰撞检测解决的是:
场景里有很多物体,怎么高效判断谁和谁碰撞。
如果有 1000 个物体,直接两两检测大概需要:
1000 * 999 / 2 = 499500 次
每帧都这样做会很贵。
所以物理引擎通常分成两步:
Broad Phase:宽阶段,快速筛出可能碰撞的物体对。
Narrow Phase:窄阶段,对候选物体对做精确碰撞检测。
一句话:
Broad Phase 负责少算。
Narrow Phase 负责算准。
跟着代码筛一次碰撞对
先用二维 AABB 跑通宽阶段。把包围盒类型放在 Aabb.cs:
using System.Numerics;
readonly record struct Aabb(Vector2 Min, Vector2 Max)
{
public bool Overlaps(Aabb other)
{
bool separated =
Max.X < other.Min.X ||
Min.X > other.Max.X ||
Max.Y < other.Min.Y ||
Min.Y > other.Max.Y;
return !separated;
}
}
准备三个物体:
using System;
using System.Numerics;
Aabb player = new(
new Vector2(0, 0),
new Vector2(2, 2));
Aabb enemy = new(
new Vector2(1, 1),
new Vector2(3, 3));
Aabb tree = new(
new Vector2(10, 10),
new Vector2(12, 12));
Console.WriteLine(player.Overlaps(enemy)); // True
Console.WriteLine(player.Overlaps(tree)); // False
player 与 enemy 在 X、Y 两个方向都有重叠,所以进入候选;tree 在 X 方向已经完全分开,不需要继续做精确形状检测。
第二步:从所有组合中收集候选对
var bodies = new[]
{
(Name: "Player", Bounds: player),
(Name: "Enemy", Bounds: enemy),
(Name: "Tree", Bounds: tree)
};
for (int i = 0; i < bodies.Length; i++)
{
for (int j = i + 1; j < bodies.Length; j++)
{
if (!bodies[i].Bounds.Overlaps(bodies[j].Bounds))
continue;
Console.WriteLine(
$"候选对: {bodies[i].Name} - {bodies[j].Name}");
}
}
输出只有:
候选对: Player - Enemy
注意它仍然叫“候选对”。AABB 重叠不保证内部的旋转多边形真的接触,只保证它们可能接触。
第三步:只对候选对做窄阶段
foreach ((Shape A, Shape B) pair in broadPhasePairs)
{
CollisionResult result = Sat.Test(pair.A, pair.B);
if (result.Intersects)
Resolve(pair, result.Normal, result.Depth);
}
这段是项目接口示意:broadPhasePairs 来自宽阶段;Sat.Test 才检查凸形状在各条候选轴上的投影;只有确认相交后才进入响应。
完整运行链是:
3 个物体产生 3 个两两组合
-> AABB 快速排除 Player-Tree、Enemy-Tree
-> 只把 Player-Enemy 送入 SAT
-> SAT 返回是否相交、法线和穿透深度
-> 响应层决定推开、反弹或触发事件
后文的 OBB、BVH 和 SAT 分别改进包围体精度、候选查询效率和窄阶段准确性,但不会改变“先少算,再算准”的调用顺序。
Broad Phase
Broad Phase 不追求精确。
它只回答:
这两个物体有没有可能碰撞?
如果两个物体明显离得很远,就直接排除。
常见做法:
AABB 粗包围
空间 Hash / 网格
四叉树 / 八叉树
BVH
Sweep and Prune
它的核心目标是:
用便宜判断,排除大量不可能碰撞的对象。
Narrow Phase
Narrow Phase 处理 Broad Phase 筛出来的候选对。
Broad Phase 只会说:
它们可能碰撞。
Narrow Phase 才会确认:
它们是不是真的碰撞。
并且计算碰撞信息:
碰撞点
碰撞法线
穿透深度
最小分离方向
常见精确检测包括:
球和球
胶囊体和胶囊体
AABB 和 AABB
OBB 和 OBB
凸多边形和凸多边形
三角形网格
可能用到的算法:
SAT
GJK
EPA
CCD
AABB
AABB 全名:
Axis-Aligned Bounding Box
轴对齐包围盒
它的边永远和世界坐标轴对齐。
2D 里可以理解成一个不旋转的矩形:
+---------+
| |
| 物体 |
| |
+---------+
3D 里通常用两个点表示:
minX, minY, minZ
maxX, maxY, maxZ
判断两个 AABB 是否相交很便宜:
bool Intersects(AABB a, AABB b)
{
if (a.Max.x < b.Min.x || a.Min.x > b.Max.x) return false;
if (a.Max.y < b.Min.y || a.Min.y > b.Max.y) return false;
if (a.Max.z < b.Min.z || a.Min.z > b.Max.z) return false;
return true;
}
优点:
判断快
实现简单
适合 Broad Phase
缺点:
不会跟着物体旋转
旋转物体可能包得很松
误报较多
误报没关系,因为后面还有 Narrow Phase 精筛。
Unity 里的 Bounds
Unity 里的:
Renderer.bounds
Collider.bounds
Bounds.Intersects(...)
这些 Bounds 是世界空间 AABB。
也就是说:
它永远和世界坐标轴对齐。
物体旋转后,bounds 可能变大,因为 Unity 要用一个不旋转的盒子把它包住。
注意:
Collider.bounds 是包围盒。
Collider 本身的真实碰撞形状不一定是 AABB。
例如 BoxCollider 会跟随 Transform 旋转,从碰撞形状上更接近 OBB。
Unity 3D 物理主要由 PhysX 处理:
Broad Phase 用粗包围结构筛候选。
Narrow Phase 对 BoxCollider、SphereCollider、CapsuleCollider、MeshCollider 等具体形状做精确检测。
Unity 2D 物理则主要由 Box2D 处理。
OBB
OBB 全名:
Oriented Bounding Box
有方向包围盒
它可以跟着物体旋转。
AABB 包旋转长条可能是:
+-------------+
| / |
| / |
| / |
+-------------+
OBB 更贴合:
+---+
/ /
/ /
+---+
优点:
比 AABB 更贴合旋转物体
误报更少
缺点:
判断更复杂
性能比 AABB 贵
通常需要 SAT 这类算法
可以粗略记:
AABB:便宜但粗。
OBB:更贴合但更贵。
BVH
BVH 全名:
Bounding Volume Hierarchy
包围体层次结构
它不是一种盒子,而是一种组织方式。
核心思想:
用大包围盒包住一组小包围盒。
再用更大的包围盒包住更多组。
形成一棵树。
例如:
Root 包住全部
├── Left 包住 A B C D
│ ├── 包住 A B
│ └── 包住 C D
└── Right 包住 E F G H
├── 包住 E F
└── 包住 G H
查询时,如果和某个大包围盒不相交,就可以跳过整个子树。
BVH 的价值是:
一次排除一大片对象。
常见用途:
射线检测
三角形网格碰撞
物理引擎 Broad Phase
渲染加速
光线追踪
场景查询
AABB / OBB 是:
用什么形状包住物体。
BVH 是:
怎么把这些包围体组织成层级结构。
SAT
SAT 全名:
Separating Axis Theorem
分离轴定理
常用于:
凸多边形碰撞
旋转矩形碰撞
OBB 碰撞
它的核心是:
如果两个凸形状没有相交,一定存在一条轴,让它们投影到这条轴后互不重叠。
反过来:
如果所有候选轴上的投影都重叠,那么两个形状相交。
SAT 的投影
对某条轴 axis,把形状的所有顶点投影上去:
float value = Vector2.Dot(point, axis);
对所有顶点取最小和最大值:
[min, max]
这就是形状在这条轴上的投影区间。
如果两个区间不重叠:
if (aMax < bMin || bMax < aMin)
{
return false;
}
说明找到分离轴,不碰撞。
如果所有候选轴都重叠,说明碰撞。
SAT 流程
伪代码:
bool Intersects(Polygon a, Polygon b)
{
foreach (var axis in GetAxes(a, b))
{
Project(a, axis, out float aMin, out float aMax);
Project(b, axis, out float bMin, out float bMax);
if (aMax < bMin || bMax < aMin)
{
return false;
}
}
return true;
}
候选轴通常来自:
多边形每条边的法线方向。
两个 2D 旋转矩形通常检查:
A 的两个局部轴
B 的两个局部轴
两个 3D OBB 通常最多检查 15 个轴:
A 的 3 个局部轴
B 的 3 个局部轴
A 的轴和 B 的轴两两叉乘得到的 9 个轴
进阶:SAT 的限制
SAT 适合凸形状。
例如:
矩形
三角形
六边形
凸包
不适合直接处理凹形状:
L 形
U 形
星形
凹形状通常要先拆成多个凸形状,再分别检测。
SAT 还能得到穿透方向
如果所有轴都重叠,可以记录每条轴上的重叠长度。
重叠长度最小的轴通常就是最小分离方向:
MTV:Minimum Translation Vector
它可以用于:
把两个物体沿最小方向分开。
计算碰撞法线。
估计穿透深度。
知识总结
碰撞检测流程:
碰撞检测通常分 Broad Phase 和 Narrow Phase。Broad Phase 用 AABB、空间划分、BVH、Sweep and Prune 等方法快速筛出可能碰撞的对象对,目标是减少候选数量。Narrow Phase 再对候选对做精确形状检测,比如球、胶囊体、OBB、凸多边形,可能用 SAT、GJK 等算法。确认碰撞后,会生成碰撞点、法线、穿透深度等信息,后续交给物理求解器或游戏逻辑处理。
AABB、OBB 与 BVH:
AABB 是轴对齐包围盒,判断便宜但对旋转物体包得较松;OBB 是有方向包围盒,可以跟随物体旋转,包得更紧但检测更贵;BVH 是包围体层次结构,用一棵包围盒树组织对象,可以快速跳过大量不相交对象。
SAT:
SAT 用于凸形状碰撞检测。它会把两个形状投影到候选分离轴上,如果任意一条轴上的投影区间不重叠,就说明不碰撞;如果所有候选轴上都重叠,就说明碰撞。候选轴通常来自多边形边的法线方向,也可以通过最小重叠轴得到碰撞法线和穿透深度。
最重要的收获
Broad Phase 负责少算。
Narrow Phase 负责算准。
AABB 便宜但粗。
OBB 更贴合但更贵。
BVH 用层级包围盒一次跳过一大片对象。
SAT 通过寻找分离轴判断凸形状是否碰撞。