游戏开发实战:用分离轴定理(SAT)搞定Unity 2D碰撞检测(附C#代码)
游戏开发实战用分离轴定理SAT搞定Unity 2D碰撞检测附C#代码在2D游戏开发中碰撞检测是构建沉浸式体验的核心技术之一。无论是角色与环境的互动、子弹命中判定还是物理模拟的精确性都依赖于高效可靠的碰撞系统。Unity引擎虽然提供了基础的碰撞组件但在处理复杂形状或需要更高性能的场景时开发者往往需要自己实现底层算法。本文将深入探讨分离轴定理SAT这一专业级碰撞检测技术通过可落地的C#代码示例帮助开发者掌握从理论到实践的完整实现路径。1. 为什么需要SAT超越Unity原生碰撞系统Unity内置的2D碰撞系统如BoxCollider2D和CircleCollider2D对于简单场景已经足够但在以下场景会显现局限性旋转物体精度问题当矩形碰撞体旋转后Unity实际使用的是轴对齐包围盒AABB的近似计算导致检测区域与实际显示不匹配复杂形状支持不足多边形碰撞体PolygonCollider2D在顶点较多时性能下降明显定制化需求特殊碰撞响应如穿透检测、滑动处理需要访问底层算法逻辑// Unity原生碰撞检测的典型问题示例 void OnCollisionEnter2D(Collision2D col) { // 当两个旋转的矩形相交时可能无法准确触发 Debug.Log(碰撞发生 col.gameObject.name); }SAT算法相比传统方法的优势检测方法精度旋转支持性能适用形状AABB低不支持高矩形圆形检测高支持高圆形SAT本文高支持中任意凸多边形网格检测极高支持低任意形状2. SAT原理精要数学背后的游戏逻辑分离轴定理的核心思想可以概括为如果存在一条直线能够将两个凸多边形完全分开那么这两个多边形不相交。具体实现时需要三个关键步骤获取潜在分离轴每个多边形的每条边的法线都是候选轴投影计算将两个多边形的所有顶点投影到当前检测轴上重叠判断检查两个投影区间是否有重叠// 向量结构体定义简化版 public struct Vector2 { public float x, y; public static float Dot(Vector2 a, Vector2 b) { return a.x * b.x a.y * b.y; } public Vector2 Normalize() { float mag Mathf.Sqrt(x * x y * y); return new Vector2(x / mag, y / mag); } public Vector2 Perpendicular() { return new Vector2(y, -x); // 获得法线向量 } }重要提示SAT仅适用于凸多边形。如果使用凹多边形需要先进行三角剖分或凸分解处理。3. 实战实现从OBB检测到完整SAT系统3.1 OBB碰撞检测实现定向包围盒OBB是SAT最典型的应用场景以下是完整的C#实现public class OBB { public Vector2 center; public Vector2[] axes new Vector2[2]; // 本地坐标轴 public Vector2[] extents new Vector2[2]; // 半长向量 public bool Intersects(OBB other) { // 检测4条可能的分离轴 if (!CheckAxis(axes[0], other)) return false; if (!CheckAxis(axes[1], other)) return false; if (!CheckAxis(other.axes[0], other)) return false; if (!CheckAxis(other.axes[1], other)) return false; return true; } private bool CheckAxis(Vector2 axis, OBB other) { axis axis.Normalize(); // 计算当前OBB在轴上的投影半径 float r1 Mathf.Abs(Vector2.Dot(extents[0], axis)) Mathf.Abs(Vector2.Dot(extents[1], axis)); // 计算另一个OBB在轴上的投影半径 float r2 Mathf.Abs(Vector2.Dot(other.extents[0], axis)) Mathf.Abs(Vector2.Dot(other.extents[1], axis)); // 计算中心点连线在轴上的投影 Vector2 centerVec center - other.center; float distance Mathf.Abs(Vector2.Dot(centerVec, axis)); return distance (r1 r2); } }3.2 通用凸多边形检测扩展上述基础实现处理任意凸多边形public class ConvexPolygon { public Vector2[] vertices; public bool Intersects(ConvexPolygon other) { // 检查当前多边形的所有边 for (int i 0; i vertices.Length; i) { Vector2 edge vertices[(i 1) % vertices.Length] - vertices[i]; Vector2 axis edge.Perpendicular().Normalize(); if (!OverlapOnAxis(axis, other)) return false; } // 检查另一个多边形的所有边 for (int i 0; i other.vertices.Length; i) { Vector2 edge other.vertices[(i 1) % other.vertices.Length] - other.vertices[i]; Vector2 axis edge.Perpendicular().Normalize(); if (!OverlapOnAxis(axis, other)) return false; } return true; } private bool OverlapOnAxis(Vector2 axis, ConvexPolygon other) { float min1 float.MaxValue, max1 float.MinValue; float min2 float.MaxValue, max2 float.MinValue; // 计算当前多边形在轴上的投影范围 foreach (var vertex in vertices) { float projection Vector2.Dot(vertex, axis); min1 Mathf.Min(min1, projection); max1 Mathf.Max(max1, projection); } // 计算另一个多边形在轴上的投影范围 foreach (var vertex in other.vertices) { float projection Vector2.Dot(vertex, axis); min2 Mathf.Min(min2, projection); max2 Mathf.Max(max2, projection); } // 检查投影是否重叠 return max1 min2 max2 min1; } }4. 性能优化与工程实践4.1 分层检测策略在实际游戏中通常会采用分层检测策略平衡精度与性能Broad Phase粗略检测使用空间分区四叉树/网格基于AABB的快速剔除减少需要精确检测的对象对Narrow Phase精确检测对通过粗略检测的对象使用SAT根据形状选择最优算法圆形用距离检测矩形用OBB等// 四叉树实现示例 public class QuadTree { private Rect bounds; private int maxObjects; private ListCollider objects; private QuadTree[] nodes; public void Insert(Collider collider) { if (nodes ! null) { int index GetIndex(collider.Bounds); if (index ! -1) { nodes[index].Insert(collider); return; } } objects.Add(collider); if (objects.Count maxObjects level MAX_LEVELS) { if (nodes null) Split(); int i 0; while (i objects.Count) { int index GetIndex(objects[i].Bounds); if (index ! -1) { nodes[index].Insert(objects[i]); objects.RemoveAt(i); } else { i; } } } } }4.2 常见问题解决方案穿透问题在高速移动物体中添加连续碰撞检测CCD性能热点对静态物体使用缓存投影计算结果浮点误差引入小量容差值epsilon进行比较// 处理高速物体穿透的射线检测法 public class Projectile : MonoBehaviour { public float speed; public LayerMask collisionMask; private Vector2 prevPosition; void Update() { prevPosition transform.position; transform.Translate(Vector2.right * speed * Time.deltaTime); // 检查移动轨迹上的碰撞 RaycastHit2D hit Physics2D.Linecast( prevPosition, transform.position, collisionMask); if (hit.collider ! null) { OnHit(hit); } } }5. Unity集成与调试技巧5.1 可视化调试工具在Scene视图中实时显示碰撞检测状态void OnDrawGizmos() { // 绘制OBB轮廓 Gizmos.color Intersects(other) ? Color.red : Color.green; Vector2[] corners GetCorners(); for (int i 0; i corners.Length; i) { int next (i 1) % corners.Length; Gizmos.DrawLine(corners[i], corners[next]); } // 绘制当前检测的分离轴 if (debugAxis ! Vector2.zero) { Gizmos.color Color.blue; Vector2 center (center other.center) * 0.5f; Gizmos.DrawLine(center, center debugAxis * 2); } }5.2 Inspector参数配置创建自定义编辑器增强工作流[CustomEditor(typeof(SATCollider))] public class SATColliderEditor : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); SATCollider collider (SATCollider)target; if (GUILayout.Button(Update Vertices)) { collider.UpdateVertices(); } EditorGUILayout.LabelField(Debug Info, EditorStyles.boldLabel); EditorGUILayout.Toggle(Colliding, collider.IsColliding); } }在实现过程中一个常见的陷阱是忘记归一化分离轴向量这会导致投影计算错误。另一个实际经验是对于移动平台将SAT计算放在Job System中并行处理可以显著提升性能。当处理大量碰撞体时建议采用对象池模式复用碰撞检测结果数据结构。