Unity游戏开发:基于分离轴定理的多边形碰撞检测实现

发布时间:2026/7/30 7:56:32
Unity游戏开发:基于分离轴定理的多边形碰撞检测实现 1. 项目概述为什么多边形碰撞检测是游戏物理的基石在游戏开发里碰撞检测是让虚拟世界“真实”起来的第一步。无论是角色踩上平台、子弹击中敌人还是车辆撞上障碍物背后都有一套算法在默默计算两个物体是否发生了接触。对于简单的形状比如圆形和矩形Unity内置的物理引擎如BoxCollider2D和CircleCollider2D已经做得非常出色开箱即用。但当我们想做一个不规则的岛屿、一个复杂的角色轮廓或者一个可破坏物体的碎片时这些基本形状就力不从心了。这时多边形碰撞检测Polygon Collision Detection就成了我们必须掌握的技能。你可能听说过“分离轴定理”Separating Axis Theorem, SAT它是实现多边形碰撞检测最经典、最高效的算法之一。而二维叉乘正是理解和实现这个定理的数学钥匙。很多教程一上来就扔出一堆公式和代码让人望而生畏。但我想说的是它的核心思想非常直观如果两个凸多边形没有碰撞那么一定能找到一条直线轴能将它们完全分开。我们的任务就是尝试找到这条“分离轴”。二维叉乘在这里扮演了双重角色一是帮我们找到所有需要检测的“候选轴”二是提供了一种优雅的方法来计算多边形在这些轴上的“投影”并判断投影是否重叠。这个项目就是带你从零开始在Unity中亲手实现一套基于分离轴定理的多边形碰撞检测系统。我们不依赖复杂的第三方库就用最基础的向量数学一步步推导最后封装成简洁可用的C#类。你会发现理解了二维叉乘的几何意义整个算法就像搭积木一样清晰。文末会提供完整的、可直接复用的代码你可以立刻把它用到你的2D游戏项目中去处理那些奇形怪状的碰撞体。2. 核心原理拆解二维叉乘与分离轴定理的深度绑定在深入代码之前我们必须把地基打牢。二维叉乘Cross Product在三维空间里结果是一个向量但在二维空间里它被定义为一个标量一个数字。对于两个二维向量a (a_x, a_y) 和b (b_x, b_y)它们的叉乘定义为float cross a.x * b.y - a.y * b.x;这个简单的公式蕴含着丰富的几何信息。它的绝对值等于以a和b为邻边构成的平行四边形的面积。它的正负号则指示了b相对于a的旋转方向如果叉乘为正b在a的逆时针方向为负则在顺时针方向为零则两向量共线。这个特性在计算多边形的法向量即垂直于边的向量时至关重要因为多边形的每条边都可以看作一个向量而它的法向量正是我们寻找“分离轴”的关键。分离轴定理SAT的核心步骤可以分解为四步寻找候选轴对于两个凸多边形取出它们所有边的法向量。这些法向量就是潜在的分离轴。为什么是边的法向量想象一下用一根筷子轴去分开两个积木最有可能成功的筷子方向就是沿着某个积木的棱边的方向或者是垂直于这个棱的方向。在2D中我们取垂直于边的方向即法向量作为检测轴。将多边形投影到轴上把多边形A和多边形B的所有顶点都“压扁”到当前检测的这条轴上得到两条线段投影。投影是一个标量范围例如[minA, maxA]和[minB, maxB]。检查投影是否重叠判断这两个标量范围是否有交集。如果没有交集那么在当前这条轴上两个多边形是分离的这意味着它们在整个空间中也必定没有碰撞。算法可以立即返回“无碰撞”。遍历所有轴如果对于每一个候选轴两个多边形的投影都发生了重叠那么恭喜没有任何一条轴能将它们分开结论就是两个多边形碰撞了。这里二维叉乘优雅地解决了第一步和第二步中的关键计算。计算边的法向量可以通过对边向量进行叉乘与一个基准向量如(0,0,1)后再取垂直来得到但更直接的方法是交换边向量的x,y分量并取反其中一个这本质上也是叉乘思想的体现。而在某些实现中利用叉乘的性质可以更高效地处理投影和重叠判断。注意分离轴定理只对凸多边形保证正确性。如果你的多边形是凹的比如一个“凹”字形需要先将其分解为多个凸多边形这个过程称为凸分解再分别进行检测。本文实现的代码默认处理凸多边形。3. 实战准备在Unity中构建我们的测试场理论说得再多不如动手一试。我们首先在Unity中搭建一个简单的测试环境。打开Unity创建一个新的2D项目。创建测试多边形在场景中创建两个空的GameObject分别命名为PolygonA和PolygonB。我们将通过脚本动态生成它们的形状。为了可视化给它们添加SpriteRenderer组件可以使用一个简单的白色Sprite或者留空因为我们主要用LineRenderer来画轮廓。绘制轮廓为每个GameObject添加一个LineRenderer组件。在检查器中设置合适的材质比如默认的Default-Line和颜色如红色和蓝色调整Width如0.1。取消勾选Loop我们稍后在代码中闭合它。创建碰撞检测管理器创建一个空的GameObject命名为CollisionManager并挂载我们即将编写的核心脚本SATCollisionDetector.cs。这个脚本将持有对PolygonA和PolygonB的引用并在每帧进行检测和可视化。准备核心C#脚本在Project窗口中创建Scripts文件夹然后创建两个C#脚本PolygonShape.cs和SATCollisionDetector.cs。PolygonShape.cs用于存储和操作多边形的数据顶点、边、法向量等SATCollisionDetector.cs则包含主要的检测逻辑和测试代码。这个测试场的目的是让我们能够实时调整多边形的顶点位置通过代码或简单的UI并立即看到碰撞检测的结果比如改变轮廓颜色或输出Log。这种即时反馈对于理解算法行为至关重要。4. 数据结构设计PolygonShape类的构建一个好的数据结构是高效算法的前提。我们的PolygonShape类需要存储多边形的核心几何信息并能根据顶点列表自动计算出边和法向量。using UnityEngine; using System.Collections.Generic; public class PolygonShape : MonoBehaviour { // 多边形的世界坐标顶点列表 public ListVector2 worldVertices new ListVector2(); // 多边形的边向量列表从当前顶点指向下一个顶点 public ListVector2 edges new ListVector2(); // 每条边的法向量单位化即分离轴的候选方向 public ListVector2 normals new ListVector2(); // 一个标识符用于调试 public string polygonName Polygon; void Start() { // 初始化从Transform的子节点或默认形状获取顶点 InitializeVertices(); CalculateEdgesAndNormals(); } // 初始化顶点这里以一个简单的四边形为例你可以扩展为从编辑器赋值或子物体获取 public void InitializeVertices() { worldVertices.Clear(); // 示例一个矩形以物体自身位置为中心 float halfWidth 1f; float halfHeight 0.5f; worldVertices.Add(transform.position new Vector3(-halfWidth, -halfHeight)); worldVertices.Add(transform.position new Vector3(halfWidth, -halfHeight)); worldVertices.Add(transform.position new Vector3(halfWidth, halfHeight)); worldVertices.Add(transform.position new Vector3(-halfWidth, halfHeight)); UpdateLineRenderer(); } // 核心计算根据顶点计算边和法向量 public void CalculateEdgesAndNormals() { edges.Clear(); normals.Clear(); int vertexCount worldVertices.Count; for (int i 0; i vertexCount; i) { // 当前顶点 Vector2 currentVert worldVertices[i]; // 下一个顶点循环到第一个 Vector2 nextVert worldVertices[(i 1) % vertexCount]; // 计算边向量从current指向next Vector2 edge nextVert - currentVert; edges.Add(edge); // 计算法向量垂直于边向量并单位化。 // 2D中向量(x, y)的法向量可以是(-y, x)或(y, -x)它们方向相反。 // 我们通常取(-y, x)来获得向外的法线假设顶点是逆时针顺序。 Vector2 normal new Vector2(-edge.y, edge.x).normalized; normals.Add(normal); } } // 更新LineRenderer以绘制多边形轮廓 public void UpdateLineRenderer() { LineRenderer lr GetComponentLineRenderer(); if (lr ! null) { lr.positionCount worldVertices.Count 1; // 1 用于闭合图形 for (int i 0; i worldVertices.Count; i) { lr.SetPosition(i, new Vector3(worldVertices[i].x, worldVertices[i].y, 0)); } // 闭合将最后一个点设置回第一个点 lr.SetPosition(worldVertices.Count, new Vector3(worldVertices[0].x, worldVertices[0].y, 0)); } } // 在Scene视图中绘制Gizmos便于调试 void OnDrawGizmosSelected() { if (worldVertices.Count 0) { Gizmos.color Color.green; for (int i 0; i worldVertices.Count; i) { int next (i 1) % worldVertices.Count; Gizmos.DrawLine(worldVertices[i], worldVertices[next]); } } } }这个类封装了多边形的基本属性。CalculateEdgesAndNormals方法是关键它遍历顶点计算每条边并通过new Vector2(-edge.y, edge.x)巧妙地利用二维向量性质得到了单位法向量。这里假设顶点是按逆时针顺序排列的这是计算机图形学中的常见约定能保证法向量指向多边形外部。5. 算法核心实现SATCollisionDetector类详解有了数据结构我们就可以实现分离轴定理的核心检测逻辑了。我们将创建一个静态工具类SATCollision来存放纯数学函数以及一个SATCollisionDetectorMonoBehaviour类来驱动测试。首先创建SATCollision静态类它不依赖于Unity的MonoBehaviour只包含算法逻辑using UnityEngine; using System.Collections.Generic; public static class SATCollision { // 核心检测函数判断两个凸多边形是否碰撞 public static bool CheckCollision(PolygonShape polyA, PolygonShape polyB, ref Vector2 minimumOverlapAxis) { // 初始化最小重叠量为一个极大值 float minOverlap float.MaxValue; minimumOverlapAxis Vector2.zero; // 1. 检查多边形A的所有边法线分离轴 if (!CheckOverlapOnAxes(polyA.normals, polyA, polyB, ref minOverlap, ref minimumOverlapAxis)) { return false; // 在某一轴上分离无碰撞 } // 2. 检查多边形B的所有边法线分离轴 if (!CheckOverlapOnAxes(polyB.normals, polyA, polyB, ref minOverlap, ref minimumOverlapAxis)) { return false; // 在某一轴上分离无碰撞 } // 所有轴上都重叠发生碰撞 // 此时 minimumOverlapAxis 存储的是最小分离向量MTV的方向minOverlap是其大小 // 可用于碰撞响应将物体推开 return true; } // 在给定的轴集合上检查两个多边形的投影是否重叠 private static bool CheckOverlapOnAxes(ListVector2 axes, PolygonShape polyA, PolygonShape polyB, ref float minOverlap, ref Vector2 minOverlapAxis) { foreach (Vector2 axis in axes) { // 将轴单位化确保投影计算准确 Vector2 normalizedAxis axis.normalized; // 计算多边形A和B在当前轴上的投影标量范围 ProjectPolygon(normalizedAxis, polyA.worldVertices, out float minA, out float maxA); ProjectPolygon(normalizedAxis, polyB.worldVertices, out float minB, out float maxB); // 检查两个投影范围是否重叠 if (!IsOverlapping(minA, maxA, minB, maxB)) { // 投影不重叠找到分离轴肯定无碰撞 return false; } else { // 投影重叠计算重叠量 float overlap GetOverlap(minA, maxA, minB, maxB); // 记录最小的重叠量及其对应的轴方向 // 这个信息对于碰撞响应计算最小平移向量MTV非常有用 if (overlap minOverlap) { minOverlap overlap; minOverlapAxis normalizedAxis; } } } return true; // 在所有给定轴上均重叠 } // 将多边形投影到一条单位轴上得到投影的最小值和最大值 private static void ProjectPolygon(Vector2 axis, ListVector2 vertices, out float min, out float max) { // 初始化用第一个顶点的投影值 min Vector2.Dot(axis, vertices[0]); max min; for (int i 1; i vertices.Count; i) { // 点积dot product就是顶点在轴上的投影长度 float projection Vector2.Dot(axis, vertices[i]); if (projection min) min projection; if (projection max) max projection; } } // 判断两个标量范围 [min1, max1] 和 [min2, max2] 是否重叠 private static bool IsOverlapping(float min1, float max1, float min2, float max2) { // 不重叠的条件一个范围完全在另一个范围的左边或右边 return !(max1 min2 || max2 min1); } // 计算两个重叠范围的重叠量 private static float GetOverlap(float min1, float max1, float min2, float max2) { // 重叠量是“两个最大值中较小的”减去“两个最小值中较大的” return Mathf.Min(max1, max2) - Mathf.Max(min1, min2); } // 高级功能计算最小平移向量MTV用于碰撞响应 public static bool CheckCollisionWithMTV(PolygonShape polyA, PolygonShape polyB, out Vector2 mtv) { mtv Vector2.zero; float minOverlap float.MaxValue; Vector2 minAxis Vector2.zero; if (!CheckCollision(polyA, polyB, ref minOverlap, ref minAxis)) { return false; // 无碰撞 } // 确定MTV的方向需要确保MTV的方向是将A从B中推出来。 // 一个简单的启发式方法是检查从A中心指向B中心的向量与最小分离轴的点积。 // 如果点积为正说明轴方向大致是从A指向B那么MTV方向应取反才能将A推离B。 Vector2 centerA GetCentroid(polyA.worldVertices); Vector2 centerB GetCentroid(polyB.worldVertices); Vector2 centerAToB centerB - centerA; if (Vector2.Dot(centerAToB, minAxis) 0) { minAxis -minAxis; } mtv minAxis * minOverlap; return true; } // 计算多边形质心顶点平均值适用于凸多边形 private static Vector2 GetCentroid(ListVector2 vertices) { Vector2 centroid Vector2.zero; foreach (Vector2 v in vertices) { centroid v; } centroid / vertices.Count; return centroid; } }这个静态类包含了SAT算法的完整实现。CheckCollision是主入口它依次用两个多边形的所有法向量作为轴进行检测。ProjectPolygon函数利用向量的点积将顶点投影到轴上这是整个算法中计算量相对较大的部分但逻辑清晰。GetOverlap函数计算的重叠量不仅用于判断其最小值正是我们进行碰撞响应时需要的“最小穿透深度”。CheckCollisionWithMTV函数是一个非常有用的扩展。它不仅在碰撞时返回true还计算出一个“最小平移向量”Minimum Translation Vector, MTV。这个向量的方向指明了将两个物体分开的最短路径方向大小则是需要移动的距离。在物理引擎中这个MTV可以直接用来解决碰撞让物体看起来是被“弹开”或“推开”而不是相互嵌入。6. 驱动与可视化SATCollisionDetector MonoBehaviour最后我们需要一个脚本来驱动检测并可视化结果。这个脚本将挂在我们的CollisionManager上。using UnityEngine; public class SATCollisionDetector : MonoBehaviour { public PolygonShape polygonA; public PolygonShape polygonB; [Header(Debug Visualization)] public bool drawNormals false; public float normalLength 0.5f; public Color collisionColor Color.red; public Color noCollisionColor Color.green; private LineRenderer lrA; private LineRenderer lrB; void Start() { if (polygonA null || polygonB null) { Debug.LogError(请将PolygonA和PolygonB拖拽到SATCollisionDetector脚本上。); return; } lrA polygonA.GetComponentLineRenderer(); lrB polygonB.GetComponentLineRenderer(); } void Update() { // 每帧更新多边形的世界坐标顶点如果多边形可能移动 UpdatePolygonVertices(polygonA); UpdatePolygonVertices(polygonB); // 重新计算边和法向量 polygonA.CalculateEdgesAndNormals(); polygonB.CalculateEdgesAndNormals(); // 执行碰撞检测 Vector2 mtv Vector2.zero; bool isColliding SATCollision.CheckCollisionWithMTV(polygonA, polygonB, out mtv); // 根据碰撞结果更新可视化例如改变线条颜色 UpdateVisualization(isColliding); // 输出调试信息 if (isColliding) { Debug.Log($碰撞发生最小平移向量 (MTV): {mtv}); } } void UpdatePolygonVertices(PolygonShape poly) { // 如果你的多边形顶点是相对于自身Transform的局部坐标 // 你需要在这里将它们转换到世界坐标。 // 本例中InitializeVertices直接设置了世界坐标所以如果多边形移动了需要更新。 // 一个更健壮的做法是在PolygonShape中存储局部坐标然后通过transform.TransformPoint转换。 // 这里为了简单我们假设worldVertices已经是最新的世界坐标。 // 如果多边形GameObject被移动/旋转/缩放你需要同步更新worldVertices。 // 提示可以在PolygonShape中增加一个UpdateTransform方法根据Transform更新worldVertices。 } void UpdateVisualization(bool isColliding) { if (lrA ! null) lrA.startColor lrA.endColor isColliding ? collisionColor : noCollisionColor; if (lrB ! null) lrB.startColor lrB.endColor isColliding ? collisionColor : noCollisionColor; } // 在Scene视图中绘制法向量便于调试理解 void OnDrawGizmos() { if (!drawNormals) return; if (polygonA ! null polygonA.normals ! null) { DrawNormals(polygonA, Color.cyan); } if (polygonB ! null polygonB.normals ! null) { DrawNormals(polygonB, Color.yellow); } } void DrawNormals(PolygonShape poly, Color color) { Gizmos.color color; for (int i 0; i poly.worldVertices.Count; i) { Vector2 vertex poly.worldVertices[i]; Vector2 normal poly.normals[i]; // 从边的中点开始绘制法向量 Vector2 nextVertex poly.worldVertices[(i 1) % poly.worldVertices.Count]; Vector2 edgeMidPoint (vertex nextVertex) / 2; Gizmos.DrawLine(edgeMidPoint, edgeMidPoint normal * normalLength); } } }这个管理器脚本将一切串联起来。在Update中它持续检测两个多边形的碰撞状态并通过改变LineRenderer的颜色给出直观反馈。OnDrawGizmos中绘制法向量的功能对于调试和理解算法运行机制非常有帮助你可以清晰地看到所有被检测的“分离轴”。7. 性能优化与高级技巧基础的SAT实现已经可以工作但在实际游戏项目中尤其是物体数量多的时候我们需要考虑性能。空间划分与粗略检测不要对所有多边形两两进行SAT检测。首先使用包围盒如AABBAxis-Aligned Bounding Box进行快速剔除。如果两个物体的AABB都不相交它们的多边形肯定不相交可以直接跳过昂贵的SAT计算。Unity的Bounds类可以很方便地进行AABB相交测试。缓存与更新多边形的边和法向量在形状不变时是固定的。如果游戏中的多边形是静态的如地形或者形状变化不频繁应该缓存它们的边和法向量而不是每帧重新计算。只有在物体移动、旋转或变形后才需要更新。顶点顺序一致性确保多边形的顶点始终按统一顺序如逆时针存储。这保证了法向量方向的一致性对于MTV方向的计算很重要。可以在PolygonShape的初始化或编辑时加入验证或自动排序逻辑。分离轴提前终止我们的实现中一旦找到一条分离轴就立即返回false这已经是优化。确保你的代码逻辑没有不必要的后续计算。投影计算的SIMD优化对于顶点数量较多的多边形投影计算点积循环是热点。在支持的情况下可以考虑使用Unity的Mathematics库和Burst编译器利用SIMD指令进行并行计算可以大幅提升性能。支持旋转与缩放我们的示例中多边形的顶点是世界坐标。如果多边形附加的GameObject发生旋转或缩放你需要相应地更新worldVertices。一个标准的做法是在PolygonShape中存储本地顶点localVertices然后在Update或需要时通过transform.TransformPoint(localVertices[i])计算出世界坐标顶点。实操心得在实现碰撞响应利用MTV推开物体时一个常见的坑是“抖动”。如果两个物体在某一帧碰撞被推开下一帧由于速度或外力又嵌入一点然后再次被推开就会产生高频的微小位移看起来就像抖动。解决方法通常包括引入一个微小的容差slop当重叠量非常小时不处理或者使用更稳定的积分方法并在解决碰撞后适当调整物体的速度。8. 完整代码整合与使用示例将上述PolygonShape.csSATCollision.cs和SATCollisionDetector.cs三个脚本放入你的Unity项目。在场景中创建两个GameObject如PolygonA,PolygonB。为它们分别添加PolygonShape组件和LineRenderer组件。在PolygonShape组件上你可以点击运行后在Inspector中动态修改worldVertices列表来改变形状或者扩展脚本支持在编辑器中编辑。创建一个CollisionManagerGameObject添加SATCollisionDetector组件。将PolygonA和PolygonB的引用拖拽到检测器脚本的对应字段。运行游戏。拖动场景中的多边形当它们重叠时线条会变成红色碰撞色分离时变成绿色。你可以在Console中看到碰撞信息和MTV向量。为了更灵活地编辑多边形你可以为PolygonShape编写一个简单的编辑器脚本允许在Scene视图中拖动顶点Gizmo来实时调整形状。这超出了核心算法的范围但能极大提升开发体验。扩展挑战凹多边形碰撞尝试实现凹多边形的碰撞检测。思路是先使用算法如耳切法将凹多边形分解为多个凸多边形然后对这些凸多边形分别进行SAT检测。圆与多边形碰撞将圆形视为拥有无限多条边的正多边形实际上有更高效的方法。圆与多边形的SAT检测需要特殊处理因为圆在任意轴上的投影计算方式不同并且它的“分离轴”需要包含从圆心到多边形每个顶点的向量。碰撞响应集成利用计算出的MTV实现一个简单的物理响应。例如当检测到碰撞时将其中一个多边形的位置加上MTV使其分离。你可以进一步结合速度、质量来计算碰撞后的反弹。通过这个项目你不仅获得了一个可用的多边形碰撞检测工具更重要的是你深入理解了二维叉乘的几何应用和分离轴定理这一经典算法的每一个细节。这种从底层原理入手亲手实现核心功能的能力是解决游戏开发中各种复杂问题的关键。

相关新闻

最新新闻

日新闻

周新闻

月新闻