摘要
分离轴定理(SAT)是一种高效的凸多边形碰撞检测算法。其核心思想是通过寻找能将两物体投影分离的轴线来判断碰撞:若存在任一轴线使投影不重叠,则物体未碰撞;若所有轴线投影均重叠,则发生碰撞。SAT在游戏中广泛应用,支持任意凸形状的精确检测。2D实现需检查多边形各边法线方向作为分离轴,3D则需增加面法线和边叉积方向检测。文中提供了简洁的C#二维实现,包含向量运算、多边形投影及碰撞检测逻辑,适用于游戏开发中的复杂碰撞场景。算法优势在于比AABB/圆形检测更精确且计算高效,是物理引擎的常用基础技术。
一、什么是分离轴定理(SAT)?
1. 生活中的比喻
想象你有两块形状奇怪的饼干(比如星星形和月亮形),你想知道它们有没有碰在一起。你把它们放在桌子上,从上面看。
你可以用一根直尺(轴)放在桌面上,试着“投影”这两块饼干到直尺上。
- 如果你能找到某个方向,让两块饼干在直尺上的投影完全不重叠,那它们一定没有碰到。
- 如果无论怎么放直尺,两块饼干的投影总有重叠,那它们一定有碰撞。
这根“直尺”就是“分离轴”,这就是分离轴定理的核心思想。
2. 形象图示
假设有两个多边形A和B:
- 你把A和B分别“投影”到某个方向的轴上(比如x轴、y轴、斜着的轴)。
- 如果在某个轴上,A的投影区间和B的投影区间没有重叠,那A和B就没有碰撞。
- 如果所有轴上都重叠,那A和B就发生了碰撞。
二、SAT的原理
- 定理内容:如果两个凸多边形(或凸多面体)没有碰撞,那么一定存在一条轴(分离轴),使得它们在这条轴上的投影不重叠。
- 检测方法:只要找到一个分离轴,证明没碰撞;如果所有可能的分离轴都没有分开它们,说明碰撞了。
常用的分离轴:
- 两个物体各自的边的法线方向(2D),或面法线/边叉积方向(3D)。
三、在游戏中的实际应用
1. 游戏场景举例
- 2D平台游戏中,角色和斜坡、箱子等多边形障碍物的碰撞检测。
- 3D游戏中,两个凸多面体(比如刚体、车辆、道具)的精确碰撞检测。
2. 为什么用SAT?
- 比AABB/圆形更精确:AABB只能检测矩形,圆只能检测圆,SAT可以检测任意凸多边形/多面体。
- 效率高:只需要检测有限个轴(2D最多是两个物体所有边的法线方向),比暴力检测所有点要快很多。
3. 实际流程
以2D为例(3D原理类似):
- 找分离轴:取两个多边形所有边的法线方向作为分离轴。
- 投影:把两个多边形的所有顶点投影到每个轴上,得到区间。
- 检测重叠:如果有任何一个轴上区间不重叠,立即返回“无碰撞”。
- 全部重叠:如果所有轴都重叠,说明发生了碰撞。
四、形象小结
- SAT就像用不同方向的手电筒照射两个物体的影子,只要有一个方向影子不重叠,物体就没碰到。
- 在游戏中,SAT让你能检测任意凸形状的碰撞,比如角色和斜坡、复杂障碍物、3D刚体等。
五、代码/伪代码简要示例
// 伪代码:检测两个凸多边形是否碰撞
foreach (每个多边形的每条边)
{
axis = 这条边的法线方向;
投影A = 把A的所有顶点投影到axis上,得到区间[minA, maxA];
投影B = 把B的所有顶点投影到axis上,得到区间[minB, maxB];
if (maxA < minB || maxB < minA)
return "没有碰撞"; // 找到分离轴
}
return "发生碰撞"; // 所有轴都重叠
六、总结
- 分离轴定理是检测两个凸形状是否碰撞的高效方法。
- 实际应用:2D/3D游戏中任意凸形状的精确碰撞检测。
- 形象理解:只要能找到一个方向让两个物体的“影子”不重叠,就一定没碰撞。
下面给你一个简洁易懂的C#二维SAT(分离轴定理)碰撞检测实现,适用于任意凸多边形。代码注释详细,便于理解和扩展。
一、核心数据结构
using System;
using System.Collections.Generic;
public struct Vec2
{
public float x, y;
public Vec2(float x, float y) { this.x = x; this.y = y; }
// 向量减法
public static Vec2 operator -(Vec2 a, Vec2 b) => new Vec2(a.x - b.x, a.y - b.y);
// 点积
public float Dot(Vec2 other) => x * other.x + y * other.y;
// 法线(顺时针90度)
public Vec2 Perpendicular() => new Vec2(-y, x);
// 单位化
public Vec2 Normalized()
{
float len = (float)Math.Sqrt(x * x + y * y);
return len > 1e-6 ? new Vec2(x / len, y / len) : new Vec2(0, 0);
}
}
// 凸多边形
public class ConvexPolygon
{
public List<Vec2> vertices; // 顶点按顺时针或逆时针顺序排列
public ConvexPolygon(List<Vec2> vertices)
{
this.vertices = vertices;
}
}
二、SAT碰撞检测主函数
public static class SAT
{
// 检测两个凸多边形是否碰撞
public static bool IsColliding(ConvexPolygon polyA, ConvexPolygon polyB)
{
// 检查A和B所有边的法线方向
return !HasSeparatingAxis(polyA, polyB) && !HasSeparatingAxis(polyB, polyA);
}
// 检查polyA的所有边的法线方向是否为分离轴
private static bool HasSeparatingAxis(ConvexPolygon polyA, ConvexPolygon polyB)
{
int countA = polyA.vertices.Count;
for (int i = 0; i < countA; i++)
{
// 当前边
Vec2 p1 = polyA.vertices[i];
Vec2 p2 = polyA.vertices[(i + 1) % countA];
Vec2 edge = p2 - p1;
Vec2 axis = edge.Perpendicular().Normalized();
// 投影A
ProjectPolygon(polyA, axis, out float minA, out float maxA);
// 投影B
ProjectPolygon(polyB, axis, out float minB, out float maxB);
// 检查投影区间是否分离
if (maxA < minB || maxB < minA)
return true; // 找到分离轴,无碰撞
}
return false; // 没有分离轴
}
// 把多边形投影到轴上,得到最小最大投影值
private static void ProjectPolygon(ConvexPolygon poly, Vec2 axis, out float min, out float max)
{
min = max = poly.vertices[0].Dot(axis);
foreach (var v in poly.vertices)
{
float proj = v.Dot(axis);
if (proj < min) min = proj;
if (proj > max) max = proj;
}
}
}
三、使用示例
class Program
{
static void Main()
{
// 三角形A
var polyA = new ConvexPolygon(new List<Vec2>
{
new Vec2(0, 0),
new Vec2(2, 0),
new Vec2(1, 2)
});
// 四边形B
var polyB = new ConvexPolygon(new List<Vec2>
{
new Vec2(1, 1),
new Vec2(3, 1),
new Vec2(3, 3),
new Vec2(1, 3)
});
bool colliding = SAT.IsColliding(polyA, polyB);
Console.WriteLine("碰撞结果: " + (colliding ? "发生碰撞" : "没有碰撞"));
}
}
输出:
碰撞结果: 发生碰撞
四、说明与扩展
- 只适用于凸多边形,凹多边形需先分解为凸多边形。
- 可扩展到3D(分离轴为所有面法线和两物体边的叉积方向)。
- 可返回最小穿透向量(MTV)用于物理分离,方法是记录重叠最小的轴。