AABB碰撞检测:Slabs Method原理与Unity/Unreal实战指南

1. 项目概述:为什么AABB碰撞检测值得你花时间?

在游戏开发里,碰撞检测是个绕不开的坎。无论是角色踩到地面、子弹击中敌人,还是玩家捡起一个道具,背后都离不开它。而轴对齐包围盒,也就是我们常说的AABB,可以说是碰撞检测家族里最基础、最高效的成员之一。它的“轴对齐”特性,意味着它的边与坐标系的轴平行,这带来了一个巨大的计算优势:判断两个AABB是否相交,只需要比较它们在X、Y、Z三个轴上的最小值和最大值区间是否有重叠。这个计算简单到几乎可以忽略不计,因此在性能敏感的场合,比如物理引擎的宽阶段检测,或者需要处理海量静态物体的场景,AABB是首选。

但是,简单不等于不会出错。我见过太多项目,初期为了赶进度,随手写个if (obj1.x < obj2.x + width && ...)就了事。等到游戏物体旋转了、缩放了,或者需要更精确的检测时,bug就接踵而至:角色卡进墙里、子弹穿模、触发检测时灵时不灵。这些问题排查起来极其耗时,往往需要回头重构整个碰撞逻辑。

所以,今天我们不聊那些复杂的凸包碰撞或者连续碰撞检测,就聚焦在这个最基础、最常用,但也最容易埋坑的AABB上。我将分享一种被称为“Slabs Method”的思路,它本质上是对AABB相交测试原理的一种清晰、可扩展的理解方式。掌握了它,你不仅能快速在Unity或Unreal中写出健壮的AABB检测代码,更能深刻理解碰撞检测的数学本质,未来遇到更复杂的需求也能从容应对。无论你是刚入门的新手,还是想巩固基础的熟手,这篇指南都能帮你避开那些我亲自踩过的坑。

2. Slabs Method核心思想拆解:从“盒子”到“平板”

Slabs Method,直译过来是“平板法”或“ slabs 方法”。这个名字听起来有点抽象,但它的思想非常直观。我们不要把它想成一个高深的算法,而是看作一种理解AABB碰撞的思维方式。

2.1 什么是“Slab”?

想象一下AABB,一个方方正正的盒子。Slabs Method建议我们不要把它看作一个整体,而是将它分解。怎么分解呢?沿着每一个坐标轴的方向,这个盒子都可以被看作是两个无限延伸的“平板”夹出来的空间。

以3D空间为例,我们有X、Y、Z三个轴。对于X轴,AABB在X方向上的范围是[min.x, max.x]。我们可以想象两个无限大的平面,一个位于x = min.x,另一个位于x = max.x。这两个平面平行于YZ平面,它们之间的空间,就是物体在X轴上“占据”的区间。这一对平面夹出的无限延伸的空间带,就是一个“X轴方向的Slab”。同理,我们也有Y轴和Z轴方向的Slab。

关键点来了:一个AABB,就是这三个相互垂直的Slab(X-Slab, Y-Slab, Z-Slab)的交集。只有当一个点同时位于这三个Slab的内部时,它才在这个AABB内部。

2.2 如何用Slab思想判断碰撞?

判断两个AABB(我们叫它A和B)是否相交,Slabs Method告诉我们:如果两个AABB在所有轴向上的Slab都相交,那么它们整体就相交;只要在任何一个轴向上,它们的Slab不相交,那么两个AABB就一定分离

这其实就是我们常说的“分离轴定理”在AABB这个特例上的简化体现。分离轴定理说,如果能找到一个轴,使得两个物体在该轴上的投影不重叠,那么它们就没碰撞。对于AABB,我们只需要检查三个标准的坐标轴(X, Y, Z)就够了。

用数学表示就是,对于每一个轴i(i 属于 {x, y, z}):

  • A的Slab区间是[A.min.i, A.max.i]
  • B的Slab区间是[B.min.i, B.max.i]这两个区间相交的条件是:A.min.i <= B.max.iB.min.i <= A.max.i

这个条件比记忆“min小于max”那种公式更不容易出错,因为它对称地处理了两个物体。

注意:这里有一个非常常见的“差一错误”陷阱。很多新手会写成A.max.i > B.min.i来判断是否碰撞,这看似正确,但在边界情况(刚好接触)下,不同游戏逻辑对“碰撞”的定义可能不同。是包含边界(>=<=)还是不包含(><)?Slabs Method清晰地要求你定义区间是开区间还是闭区间。在大多数物理引擎中,包含边界的判断(即允许刚好接触)是更常用的,因为它更稳定。我们在实现时必须明确这一点。

2.3 Slabs Method的优势:不止于相交检测

理解了Slab是区间的交集,它的威力就显现出来了。我们不仅能回答“是否碰撞”,还能轻松地:

  1. 计算碰撞深度(穿透向量):如果碰撞了,在每个轴上,重叠的部分是多少?取所有轴上重叠最小的那个轴,其重叠量就是最短的分离距离,方向指向需要将物体推离的方向。这对于碰撞响应(把物体推开)至关重要。
  2. 计算接触时间(用于连续检测):在物体运动时,我们可以计算它在每个轴上进入和离开另一个AABB的Slab的时间点。取最晚的进入时间和最早的离开时间,就能得到实际发生碰撞的时间窗口。这是实现“子弹时间”或防止高速物体穿模的基础。
  3. 扩展到OBB(定向包围盒):Slabs Method的思想可以推广。对于旋转了的盒子(OBB),我们不再使用世界坐标轴,而是使用物体自身的局部坐标轴作为“Slab”的方向。虽然计算变复杂了,但核心逻辑一脉相承。

所以,Slabs Method不仅仅是一个代码写法,它提供了一个坚实的、可扩展的概念框架。接下来,我们就基于这个框架,在Unity和Unreal中实现它。

3. 在Unity中实现Slabs Method驱动的AABB检测

Unity提供了强大的Collider组件和物理引擎,但有时我们需要更轻量、更定制化的碰撞逻辑,比如用于AI感知范围、技能触发区域或者非物理对象的交互。这时,手动实现AABB检测就派上用场了。

3.1 基础数据结构与表示

在Unity中,一个AABB最自然的表示就是Bounds结构体。它包含了center(中心点)和size(尺寸),我们可以很容易地计算出minmax

public struct AABB { public Vector3 min; public Vector3 max; public AABB(Vector3 center, Vector3 size) { Vector3 extents = size * 0.5f; min = center - extents; max = center + extents; } // 也可以直接从Transform和MeshRenderer构建 public static AABB FromRenderer(MeshRenderer renderer) { Bounds bounds = renderer.bounds; return new AABB(bounds.center, bounds.size); } }

3.2 核心相交检测函数

根据Slabs Method,我们实现一个静态方法。这里我采用包含边界的判断(<=>=),因为它更通用。

public static bool Intersects(AABB a, AABB b) { // 检查X轴Slab是否相交 bool overlapX = (a.min.x <= b.max.x) && (b.min.x <= a.max.x); // 检查Y轴Slab是否相交 bool overlapY = (a.min.y <= b.max.y) && (b.min.y <= a.max.y); // 检查Z轴Slab是否相交 bool overlapZ = (a.min.z <= b.max.z) && (b.min.z <= a.max.z); // 所有轴Slab都相交,则AABB相交 return overlapX && overlapY && overlapZ; }

代码极其简单,但这就是Slabs Method的核心。你可以看到,它清晰地分离了三个轴上的判断,逻辑一目了然。

3.3 获取碰撞信息:重叠深度与法线

仅仅知道“撞了”还不够,我们通常需要知道“撞了多少”和“从哪个方向撞的”,以便做出响应(比如把角色推开)。

public static bool Intersects(AABB a, AABB b, out Vector3 penetration) { penetration = Vector3.zero; if (!Intersects(a, b)) return false; // 先用简单方法判断是否相交 // 计算在每个轴上的重叠深度(穿透量) // 重叠深度 = 两个区间中较小的max - 较大的min float overlapX = Mathf.Min(a.max.x, b.max.x) - Mathf.Max(a.min.x, b.min.x); float overlapY = Mathf.Min(a.max.y, b.max.y) - Mathf.Max(a.min.y, b.min.y); float overlapZ = Mathf.Min(a.max.z, b.max.z) - Mathf.Max(a.min.z, b.min.z); // 找出最小的重叠深度及其对应的轴 // 我们认为最小的重叠方向是最短的分离路径 float minOverlap = Mathf.Min(Mathf.Min(overlapX, overlapY), overlapZ); if (Mathf.Approximately(minOverlap, overlapX)) { // X轴重叠最小,分离方向沿X轴 penetration.x = (a.center.x < b.center.x) ? -overlapX : overlapX; // 方向由中心点相对位置决定 } else if (Mathf.Approximately(minOverlap, overlapY)) { // Y轴重叠最小,分离方向沿Y轴 penetration.y = (a.center.y < b.center.y) ? -overlapY : overlapY; } else { // Z轴重叠最小,分离方向沿Z轴 penetration.z = (a.center.z < b.center.z) ? -overlapZ : overlapZ; } return true; }

这个函数返回一个penetration向量,它的方向指出了将物体A从物体B中推离的方向,长度就是需要推开的距离。这是实现简单物理响应(如角色与墙壁碰撞)的关键数据。

实操心得:在比较浮点数判断“最小重叠轴”时,直接使用==非常危险,因为浮点数有精度误差。我强烈推荐使用Mathf.Approximately或者判断差值是否小于一个极小的阈值(如1e-5f)。否则,在两个轴重叠深度极其接近的情况下,可能会得到不稳定的碰撞法线,导致物体在角落抖动。

3.4 性能优化与批量检测

当需要检测大量AABB时(例如,一个技能同时检测范围内所有敌人),逐对检测的O(n²)复杂度是不可接受的。这时,我们可以利用空间划分数据结构,如四叉树(2D)八叉树/网格(3D)

其思想依然是Slabs Method的延伸:我们不是在全局范围比较所有物体,而是先快速判断一个AABB与空间划分的“节点”的Slab是否相交。如果连节点都不相交,那节点内的所有物体也都不可能相交。

这里给出一个非常简化的基于网格的宽阶段检测思路:

  1. 将世界空间划分为均匀的网格。
  2. 每个AABB根据其minmax坐标,计算出它覆盖了哪些网格单元格。
  3. 将AABB的引用注册到它覆盖的所有单元格中。
  4. 当检测某个AABB时,只需获取它所在单元格及相邻单元格内的其他AABB列表,进行窄阶段(精确的AABB相交)检测即可。

这极大地减少了需要两两比较的对象对数。Unity的Physics系统内部就采用了类似(但更复杂)的Broad-Phase算法。

4. 在Unreal Engine中应用Slabs Method

Unreal Engine(UE)的碰撞系统非常庞大,主要围绕PrimitiveComponent和其BodyInstance中的碰撞形状展开。手动实现AABB检测的场景与Unity类似,多用于Gameplay逻辑而非物理模拟。

4.1 使用FBox构建AABB

UE中表示AABB的类是FBox。它可以直接从很多组件获取。

// 从一个StaticMeshComponent获取其世界空间的AABB UStaticMeshComponent* MeshComp = ...; FBoxSphereBounds Bounds = MeshComp->CalcBounds(MeshComp->GetComponentTransform()); FBox AABB = Bounds.GetBox(); // 这就是一个FBox // FBox提供了Min和Max成员变量 FVector Min = AABB.Min; FVector Max = AABB.Max;

4.2 实现相交检测与信息获取

在UE中,我们可以用C++编写一个工具函数,其逻辑与Unity版本完全一致,体现了Slabs Method的跨引擎通用性。

bool AABBIntersection(const FBox& BoxA, const FBox& BoxB, FVector& OutPenetration) { OutPenetration = FVector::ZeroVector; // Slabs Method: 检查每个轴区间是否重叠 if (BoxA.Max.X < BoxB.Min.X || BoxB.Max.X < BoxA.Min.X) return false; if (BoxA.Max.Y < BoxB.Min.Y || BoxB.Max.Y < BoxA.Min.Y) return false; if (BoxA.Max.Z < BoxB.Min.Z || BoxB.Max.Z < BoxA.Min.Z) return false; // 计算重叠深度 float OverlapX = FMath::Min(BoxA.Max.X, BoxB.Max.X) - FMath::Max(BoxA.Min.X, BoxB.Min.X); float OverlapY = FMath::Min(BoxA.Max.Y, BoxB.Max.Y) - FMath::Max(BoxA.Min.Y, BoxB.Min.Y); float OverlapZ = FMath::Min(BoxA.Max.Z, BoxB.Max.Z) - FMath::Max(BoxA.Min.Z, BoxB.Min.Z); // 寻找最小重叠轴 float MinOverlap = FMath::Min3(OverlapX, OverlapY, OverlapZ); const float Tolerance = 1e-5f; if (FMath::IsNearlyEqual(MinOverlap, OverlapX, Tolerance)) { OutPenetration.X = (BoxA.GetCenter().X < BoxB.GetCenter().X) ? -OverlapX : OverlapX; } else if (FMath::IsNearlyEqual(MinOverlap, OverlapY, Tolerance)) { OutPenetration.Y = (BoxA.GetCenter().Y < BoxB.GetCenter().Y) ? -OverlapY : OverlapY; } else { OutPenetration.Z = (BoxA.GetCenter().Z < BoxB.GetCenter().Z) ? -OverlapZ : OverlapZ; } return true; }

4.3 与UE物理系统的协作

大多数时候,我们不需要手动实现这些。UE的碰撞查询系统功能强大:

  • Overlap查询:使用UWorld::OverlapMultiBy...系列函数,可以快速检测与某个形状重叠的所有物体。你可以指定一个FCollisionShape,其中就包括Box
  • Sweep查询:用于连续碰撞检测,检测一个形状沿着一条线段移动时是否会撞到什么。这本质上是在多个时间点上进行Slab测试。
  • 碰撞预设与通道:这是UE碰撞系统的精髓。通过合理设置对象的碰撞预设和响应通道,你可以精细控制什么和什么能碰撞,以及碰撞后是重叠、阻挡还是忽略。手动检测通常用于那些不适合或不想用物理引擎的Gameplay逻辑。

一个经典的使用场景:你的游戏有一个“警觉区域”,当敌人进入这个扇形区域时会被触发。你可以:

  1. 用物理系统做一个大的球形碰撞体作为触发器(简单但可能产生不必要的物理开销)。
  2. 手动用AABB做一个快速的宽阶段过滤(先判断敌人是否在AABB包围盒内),然后再进行精确的扇形检测(计算角度和距离)。这种方法更高效、更可控。这里的AABB检测,就可以用我们上面实现的Slabs Method。

5. 常见陷阱、优化策略与实战技巧

即使理解了原理,在实际项目中应用AABB碰撞,仍然有很多细节需要注意。下面是我总结的一些“坑”和应对技巧。

5.1 精度问题与浮点数误差

这是3D编程的永恒之敌。在判断边界时,直接使用><可能会因为浮点数误差导致在边界处时碰撞时而不碰撞。

  • 对策:引入一个微小的容差EPSILON(例如1e-5f)。将判断条件改为A.max.x + EPSILON > B.min.x。但要注意,这实际上略微放大了你的碰撞体。更稳健的做法是在设计游戏逻辑时,就避免让游戏对象长期处于“刚好接触”的临界状态。

5.2 动态物体的AABB更新

对于移动、旋转、缩放的物体,其世界空间的AABB需要每帧更新。

  • 低效做法:每帧根据模型的所有顶点重新计算最小/最大值。
  • 高效做法
    1. 在局部空间保存一个固定的AABB(通常是模型的包围盒)。
    2. 每帧,根据物体的变换矩阵(Transform Matrix),将这个局部AABB的8个顶点变换到世界空间。
    3. 遍历这8个顶点,快速计算出新的世界空间AABB的min和max。
    4. 对于只有平移和均匀缩放的物体,计算可以简化:worldMin = transform.localToWorldMatrix.MultiplyPoint(localMin),然后根据缩放系数调整size。但如果物体发生了旋转,局部空间的AABB变换到世界空间后,通常不再是一个轴对齐的盒子,而是一个OBB。这时如果你仍需要AABB,就必须计算顶点集的外包,或者接受一个更大的、能包裹住旋转后物体的保守AABB。

踩坑实录:早期我曾试图用物体的position和固定的size来计算AABB,忽略了物体的旋转。结果就是,一个旋转了45度的长条物体,其AABB还是一个正方向的盒子,但实际模型已经斜着伸出盒子外了,导致碰撞检测范围严重不准。切记:世界空间的AABB必须考虑物体的完整变换(旋转、缩放)

5.3 缩放与非均匀缩放的影响

如果物体被非均匀缩放(例如,X轴放大2倍,Y轴不变),其局部空间的AABB经过变换后,形状会改变。手动计算时,需要将缩放系数应用到局部AABB的extents(半尺寸)上,再结合旋转和平移。在Unity中,直接使用Renderer.boundsCollider.bounds是最省事的,它们已经处理好了这一切。在Unreal中,使用CalcBounds函数。

5.4 高速物体穿模问题

这是AABB(以及所有离散碰撞检测)的固有缺陷。如果物体一帧移动的距离超过其自身尺寸或对方物体的尺寸,它就可能从一端“瞬移”到另一端,中间没有检测到碰撞。

  • 对策:连续碰撞检测。思路是将运动过程视为一条线段(从上一帧位置到当前帧位置),检测这条线段与目标AABB是否相交。这可以通过对每个轴向上的Slab分别计算射线进入和离开的时间来实现,最后取交集。这就是Slabs Method在时间维度上的延伸。虽然计算量稍大,但对于子弹、高速移动的玩家角色来说是必要的。

5.5 调试与可视化

看不见的碰撞体是BUG的温床。务必为你的自定义AABB检测添加调试绘制功能。

  • Unity:在OnDrawGizmosOnDrawGizmosSelected中使用Gizmos.DrawWireCubeGizmos.color来绘制AABB的线框。
  • Unreal:使用DrawDebugBox函数,可以在游戏运行时或编辑器中绘制出盒子的轮廓。 清晰的调试可视化,能帮你快速确认AABB的大小、位置是否正确,是节省调试时间的利器。

6. 从AABB到更复杂形状的思维延伸

掌握了Slabs Method,你就掌握了分离轴定理的入门钥匙。当你需要处理更复杂的碰撞形状时,比如OBB(定向包围盒)、凸包甚至三角形网格,其核心思想是相通的:

  1. 找到一组潜在的分离轴(对于OBB,是每个盒子的3个本地轴;对于凸包,是每个面的法线以及边叉乘得到的轴)。
  2. 将两个物体投影到每一个轴上,得到两个区间。
  3. 如果存在任何一个轴,使得这两个投影区间不重叠,则物体分离。
  4. 如果所有轴上的投影区间都重叠,则物体相交。

AABB只是这个通用算法的一个特例,它的分离轴就是世界的X、Y、Z轴,且计算极其简单。理解了这一点,你再去看Unity的MeshCollider或Unreal的复杂碰撞体,就不会再觉得神秘。它们无非是在更多、更复杂的轴上执行同样的区间重叠测试。

所以,花时间吃透AABB和Slabs Method,绝对是一笔划算的投资。它建立起的思维模型,能让你在游戏开发的道路上走得更稳、更远。下次当你需要写碰撞检测时,不妨先在脑海里画一画那些“平板”,理一理各个轴上的区间,代码自然就会清晰、健壮起来。