从零实现GJK碰撞检测算法:原理、代码与优化详解

📅 2026/7/17 8:00:55
从零实现GJK碰撞检测算法:原理、代码与优化详解
1. 项目概述为什么我们要手写GJK碰撞检测在游戏开发中物理引擎是让虚拟世界“活”过来的核心。无论是角色跳跃、子弹命中还是箱子堆叠倒塌背后都离不开精确且高效的碰撞检测。Unity和Cocos Creator等主流引擎都内置了强大的物理系统比如Bullet、PhysX或内置的轻量引擎它们像黑盒一样为我们处理了所有复杂的物理计算。这带来了便利但也带来了两个问题一是性能开销尤其是在移动端或需要处理大量动态物体的场景二是“黑盒”带来的不可控性当遇到奇怪的穿透、抖动或性能瓶颈时我们往往难以深入排查。这就是为什么我们需要深入引擎内部亲手实现一个核心的碰撞检测算法——GJKGilbert–Johnson–Keerthi算法。GJK算法是处理凸体碰撞检测的“瑞士军刀”它高效、优雅是许多商业物理引擎包括你用的Unity和Cocos Creator的基石之一。通过这个项目你不仅能获得一个可独立运行、零依赖的碰撞检测模块用于特定性能敏感场景更能彻底理解碰撞检测的本质。当你的游戏出现诡异的物理Bug时这份底层知识将成为你排查问题的“火眼金睛”。本文将带你从零开始在UnityC#和Cocos CreatorTypeScript两种环境中手把手实现GJK算法并附上完整、可直接运行的代码。2. GJK算法核心思想与数学基础拆解在动手写代码之前我们必须先吃透GJK算法的灵魂。它之所以强大是因为它用一种非常聪明的方式将“两个凸体是否相交”这个几何问题转化为了一个在闵可夫斯基差Minkowski Difference形成的凸包中寻找是否包含原点的单纯形Simplex问题。2.1 核心概念闵可夫斯基差与支撑函数想象一下你有两个形状A和B。闵可夫斯基差A - B的定义是从B中任意取一点从A中任意取一点用A的点减去B的点得到的所有可能结果构成的集合。用数学表达就是MinkowskiDiff {a - b | a ∈ A, b ∈ B}。这个新形状有一个关键特性如果A和B相交那么原点(0,0)一定在A - B这个集合的内部。如果A和B刚好相切原点就在边界上。如果它们分离原点就在集合外部。GJK算法的目标就是判断原点是否在这个A - B集合内。但我们不可能真的去计算这个无限点的集合。这时就需要第二个核心工具支撑函数Support Function。给定一个方向向量d形状A的支撑点Support(A, d)是在这个方向上投影最远的点。对于凸多边形就是所有顶点中点乘d结果最大的那个顶点。支撑函数的计算非常快。对于闵可夫斯基差A - B其支撑点可以通过分别计算A和B的支撑点来高效获得Support(A-B, d) Support(A, d) - Support(B, -d)。注意方向对于B我们要取反方向-d。2.2 算法流程单纯形迭代与原点包含性检查GJK算法是一个迭代过程它试图在A - B形成的凸包中构建一个包含原点的单纯形。在2D中单纯形可以是点、线段或三角形。初始化选择一个初始搜索方向通常可以是A的中心指向B的中心的向量。创建一个空的单纯形列表。迭代 a. 根据当前搜索方向d利用支撑函数计算A - B上的一个新点p。 b. 将这个新点p加入单纯形。 c. 判断当前单纯形是否包含原点。在2D中 - 如果单纯形是一个点显然不包含原点除非该点就是原点那意味着A和B有一个公共点。 - 如果单纯形是一条线段我们需要判断原点是否在线段所在的直线上并且在线段的两端点之间。这可以通过向量叉积和点积来判断。 - 如果单纯形是一个三角形我们需要判断原点是否在三角形内部。这可以通过计算重心坐标或连续的同向性利用叉积判断原点是否在每条边的同一侧来实现。 d. 如果单纯形包含原点则算法结束返回碰撞true。 e. 如果单纯形不包含原点我们需要修正单纯形剔除掉那些对包含原点没有贡献的“最远点”并计算一个新的搜索方向d这个方向应该指向原点并且垂直于当前单纯形中离原点最近的部分例如对于线段就是垂直于该线段并指向原点一侧的法线。 f. 如果新的支撑点p在方向d上的投影距离已经不再增加即新点没有比之前更靠近原点说明我们已经到达了A - B凸包的边界且原点不在其内。算法结束返回无碰撞false。这个迭代过程通常收敛得非常快在几步之内就能得出结论这也是GJK高效的原因。实操心得理解“修正单纯形”和“计算新方向”是理解GJK的关键。在2D中当单纯形是线段且原点不在其上时新方向就是垂直于线段并指向原点的向量。你需要确保这个方向是“远离”当前线段的这样才能在下一步探索新的区域。3. 手把手实现Unity (C#) 版本完整代码解析我们将在一个Unity的C#脚本中实现完整的2D GJK碰撞检测。为了清晰我们会创建一个GJKCollisionDetector静态工具类。3.1 数据结构定义与支撑函数实现首先我们需要定义一些基础数据结构。在Unity中我们可以直接使用Vector2但为了教学清晰我们定义自己的Point和Shape结构。using UnityEngine; using System.Collections.Generic; public static class GJKCollisionDetector { // 表示一个2D形状由一系列顶点按顺时针或逆时针顺序构成 public class Shape { public Vector2[] vertices; public Shape(Vector2[] verts) { vertices verts; } // 支撑函数返回在给定方向d上最远的顶点 public Vector2 GetSupportPoint(Vector2 d) { float maxDot float.NegativeInfinity; Vector2 supportPoint vertices[0]; foreach (var v in vertices) { float dot Vector2.Dot(v, d); if (dot maxDot) { maxDot dot; supportPoint v; } } return supportPoint; } } // 表示闵可夫斯基差上的一个点同时记录它来自A和B的原始顶点用于后续计算碰撞信息如穿透向量 public struct MinkowskiPoint { public Vector2 point; // A - B 的结果 public Vector2 fromA; public Vector2 fromB; } }3.2 GJK核心迭代逻辑实现接下来是算法的核心。我们将迭代过程封装在一个函数中。public static bool CheckCollision(Shape shapeA, Shape shapeB) { // 1. 初始化方向从A的中心指向B的中心简单估算 Vector2 centerA CalculateCenter(shapeA); Vector2 centerB CalculateCenter(shapeB); Vector2 d centerB - centerA; if (d Vector2.zero) d Vector2.right; // 如果中心重合给一个默认方向 // 2. 获取初始支撑点构建初始单纯形一个点 MinkowskiPoint simplexPoint GetMinkowskiSupport(shapeA, shapeB, d); ListMinkowskiPoint simplex new ListMinkowskiPoint { simplexPoint }; // 3. 调整初始方向指向原点 d -simplexPoint.point; // 从第一个支撑点指向原点 // 4. 开始迭代设置最大迭代次数防止无限循环理论上很少超过10次 int maxIterations 20; for (int i 0; i maxIterations; i) { // 获取新的支撑点 MinkowskiPoint newPoint GetMinkowskiSupport(shapeA, shapeB, d); // 如果新点在当前方向上的投影没有越过原点点乘0则不可能包含原点 if (Vector2.Dot(newPoint.point, d) 0) { return false; // 分离 } simplex.Add(newPoint); // 处理单纯形判断是否包含原点并更新单纯形和方向d if (HandleSimplex(ref simplex, ref d)) { return true; // 碰撞 } } // 理论上不应走到这里出于安全返回false Debug.LogWarning(GJK reached max iterations.); return false; } // 计算形状的近似中心用于初始化方向 private static Vector2 CalculateCenter(Shape shape) { Vector2 sum Vector2.zero; foreach (var v in shape.vertices) sum v; return sum / shape.vertices.Length; } // 计算闵可夫斯基差上的支撑点 private static MinkowskiPoint GetMinkowskiSupport(Shape a, Shape b, Vector2 d) { Vector2 supportA a.GetSupportPoint(d); Vector2 supportB b.GetSupportPoint(-d); // 对B取反方向 return new MinkowskiPoint { point supportA - supportB, fromA supportA, fromB supportB }; }3.3 单纯形处理与方向更新算法核心中的核心HandleSimplex函数是GJK的“大脑”。在2D中单纯形最多有3个点三角形。private static bool HandleSimplex(ref ListMinkowskiPoint simplex, ref Vector2 d) { // 根据单纯形中点的数量进行不同处理 if (simplex.Count 2) { // 单纯形是一条线段 [a, b]其中b是最后加入的新点 return ProcessLine(ref simplex, ref d); } else if (simplex.Count 3) { // 单纯形是一个三角形 [a, b, c]其中c是最后加入的新点 return ProcessTriangle(ref simplex, ref d); } // simplex.Count 1 的情况在外部循环中处理 return false; } private static bool ProcessLine(ref ListMinkowskiPoint simplex, ref Vector2 d) { // 假设simplex [A, B]其中B是较新的点 Vector2 a simplex[0].point; Vector2 b simplex[1].point; // 向量AB, AO (O是原点即Vector2.zero) Vector2 ab b - a; Vector2 ao -a; // 从A指向原点 // 判断原点相对于线段AB的位置 // 使用叉积的标量结果在2D中相当于 perp dot product // 如果原点在AB的“左侧”假设AB方向从A到B我们需要向这个方向搜索 if (Vector2.Dot(Perp(ab), ao) 0) { // 原点在AB的一侧新的搜索方向是垂直于AB并指向原点的一侧 d TripleProduct(ab, ao, ab); // 等效于 Perp(ab) 投影到AO方向 } else { // 原点在AB的“右侧”或线上我们只需要考虑点A // 将单纯形缩减为只包含A方向指向原点 simplex.RemoveAt(1); // 移除B d ao; } return false; // 线段不可能包含原点除非原点在线段上但概率极低这里简化处理 } private static bool ProcessTriangle(ref ListMinkowskiPoint simplex, ref Vector2 d) { // 假设simplex [A, B, C]其中C是最新加入的点 Vector2 a simplex[0].point; Vector2 b simplex[1].point; Vector2 c simplex[2].point; // 计算边的法线方向垂直于边并指向三角形外部 Vector2 ab b - a; Vector2 ac c - a; Vector2 ao -a; // 检查原点是否在边AB的外侧 Vector2 abPerp Perp(ab); if (Vector2.Dot(abPerp, ao) 0) { // 原点在AB的外侧移除点C将单纯形退化为线段AB并更新方向 simplex.RemoveAt(2); // 移除C d TripleProduct(ab, ao, ab); // 指向AB外侧垂直于AB的方向 return false; } // 检查原点是否在边AC的外侧 Vector2 acPerp Perp(ac); if (Vector2.Dot(acPerp, ao) 0) { // 原点在AC的外侧移除点B将单纯形退化为线段AC simplex.RemoveAt(1); // 移除B d TripleProduct(ac, ao, ac); return false; } // 如果原点既不在AB外侧也不在AC外侧那么它就在三角形ABC内部 return true; // 碰撞发生 } // 工具函数计算2D向量的垂直向量逆时针旋转90度 private static Vector2 Perp(Vector2 v) { return new Vector2(-v.y, v.x); } // 工具函数三重积 (a × b) × c在2D中用于求垂直于a的向量在c方向上的投影常用于计算新的搜索方向 private static Vector2 TripleProduct(Vector2 a, Vector2 b, Vector2 c) { // 在2D中 (a × b) 是一个标量但我们可以用点积模拟Perp(a) * (b · Perp(c)) - Perp(c) * (b · Perp(a)) // 一个更直接且稳定的实现是Perp( Vector2.Dot(a, c) * b - Vector2.Dot(a, b) * c ) // 但为了清晰和GJK经典描述我们使用 Perp( Vector2.Dot(Perp(a), b) * Perp(a) ) 的变体。 // 这里实现一个简化稳定版返回垂直于a的向量且其方向与 (b 在垂直于a的方向上的分量) 相同。 float dot Vector2.Dot(a, b); float dot2 Vector2.Dot(a, c); return dot * c - dot2 * b; }3.4 在Unity中的使用示例创建一个测试脚本挂载到空物体上运行即可在Gizmos中看到碰撞检测结果。using UnityEngine; public class GJKTest : MonoBehaviour { public Transform shapeATransform; public Vector2[] shapeAVerticesLocal new Vector2[] { new Vector2(-1, -1), new Vector2(1, -1), new Vector2(0, 1) }; // 三角形 public Transform shapeBTransform; public Vector2[] shapeBVerticesLocal new Vector2[] { new Vector2(-0.5f, -0.5f), new Vector2(0.5f, -0.5f), new Vector2(0.5f, 0.5f), new Vector2(-0.5f, 0.5f) }; // 矩形 void Update() { // 将本地顶点转换到世界空间 Vector2[] worldVertsA TransformVertices(shapeAVerticesLocal, shapeATransform); Vector2[] worldVertsB TransformVertices(shapeBVerticesLocal, shapeBTransform); GJKCollisionDetector.Shape shapeA new GJKCollisionDetector.Shape(worldVertsA); GJKCollisionDetector.Shape shapeB new GJKCollisionDetector.Shape(worldVertsB); bool isColliding GJKCollisionDetector.CheckCollision(shapeA, shapeB); Debug.Log(Collision: isColliding); } private Vector2[] TransformVertices(Vector2[] localVerts, Transform trans) { Vector2[] worldVerts new Vector2[localVerts.Length]; for (int i 0; i localVerts.Length; i) { worldVerts[i] trans.TransformPoint(localVerts[i]); } return worldVerts; } void OnDrawGizmos() { if (shapeATransform null || shapeBTransform null) return; DrawShapeGizmo(shapeAVerticesLocal, shapeATransform, Color.green); DrawShapeGizmo(shapeBVerticesLocal, shapeBTransform, Color.blue); // 在Scene视图中实时计算并显示碰撞结果 Vector2[] worldVertsA TransformVertices(shapeAVerticesLocal, shapeATransform); Vector2[] worldVertsB TransformVertices(shapeBVerticesLocal, shapeBTransform); GJKCollisionDetector.Shape shapeA new GJKCollisionDetector.Shape(worldVertsA); GJKCollisionDetector.Shape shapeB new GJKCollisionDetector.Shape(worldVertsB); bool isColliding GJKCollisionDetector.CheckCollision(shapeA, shapeB); Gizmos.color isColliding ? Color.red : Color.white; Gizmos.DrawWireSphere(Vector3.zero, 0.1f); // 在原点画个小球示意 } void DrawShapeGizmo(Vector2[] verts, Transform trans, Color col) { Gizmos.color col; for (int i 0; i verts.Length; i) { Vector3 worldPosCurrent trans.TransformPoint(verts[i]); Vector3 worldPosNext trans.TransformPoint(verts[(i 1) % verts.Length]); Gizmos.DrawLine(worldPosCurrent, worldPosNext); } } }4. 手把手实现Cocos Creator (TypeScript) 版本完整代码解析在Cocos Creator中我们使用TypeScript实现。逻辑与C#版本完全一致只是语法和API有所不同。我们将在Cocos Creator的脚本组件中实现。4.1 类型定义与支撑函数创建一个新的TypeScript脚本例如GJKCollision.ts。import { _decorator, Component, Vec2, Color, Graphics } from cc; const { ccclass, property } _decorator; // 定义形状类 export class Shape { vertices: Vec2[]; constructor(verts: Vec2[]) { this.vertices verts; } // 支撑函数 getSupportPoint(d: Vec2): Vec2 { let maxDot -Infinity; let supportPoint this.vertices[0]; for (const v of this.vertices) { let dot v.dot(d); if (dot maxDot) { maxDot dot; supportPoint v; } } return supportPoint.clone(); } } // 闵可夫斯基点结构 export interface MinkowskiPoint { point: Vec2; fromA: Vec2; fromB: Vec2; } // GJK碰撞检测器 export class GJKCollisionDetector { static checkCollision(shapeA: Shape, shapeB: Shape): boolean { // 1. 初始化方向 let centerA this.calculateCenter(shapeA); let centerB this.calculateCenter(shapeB); let d centerB.subtract(centerA); if (d.equals(Vec2.ZERO)) { d Vec2.RIGHT; } // 2. 获取初始支撑点 let simplexPoint this.getMinkowskiSupport(shapeA, shapeB, d); let simplex: MinkowskiPoint[] [simplexPoint]; // 3. 调整方向指向原点 d simplexPoint.point.negative(); // 等价于 Vec2.ZERO.subtract(simplexPoint.point) // 4. 开始迭代 const maxIterations 20; for (let i 0; i maxIterations; i) { let newPoint this.getMinkowskiSupport(shapeA, shapeB, d); // 检查新点是否在方向d上超越了原点 if (newPoint.point.dot(d) 0) { return false; // 分离 } simplex.push(newPoint); if (this.handleSimplex(simplex, d)) { return true; // 碰撞 } // handleSimplex会修改simplex和d } console.warn(GJK reached max iterations.); return false; } private static calculateCenter(shape: Shape): Vec2 { let sum new Vec2(0, 0); for (const v of shape.vertices) { sum.add(v); } return sum.multiplyScalar(1 / shape.vertices.length); } private static getMinkowskiSupport(a: Shape, b: Shape, d: Vec2): MinkowskiPoint { let supportA a.getSupportPoint(d); let negD d.negative(); let supportB b.getSupportPoint(negD); return { point: supportA.subtract(supportB), fromA: supportA, fromB: supportB }; } private static handleSimplex(simplex: MinkowskiPoint[], d: Vec2): boolean { if (simplex.length 2) { return this.processLine(simplex, d); } else if (simplex.length 3) { return this.processTriangle(simplex, d); } return false; } private static processLine(simplex: MinkowskiPoint[], d: Vec2): boolean { let a simplex[0].point; let b simplex[1].point; let ab b.subtract(a); let ao a.negative(); // 原点指向a的向量我们需要的是a指向原点的向量即ao的负值 // 注意这里ao是a指向原点的向量即 Vec2.ZERO.subtract(a) ao new Vec2(-a.x, -a.y); // 计算垂直于ab的向量法线 let abPerp new Vec2(-ab.y, ab.x); // 逆时针旋转90度 if (abPerp.dot(ao) 0) { // 原点在ab的“左侧”新方向是垂直于ab并指向原点的一侧 // 使用三重积近似计算方向 abPerp.dot(ao) * abPerp // 但更标准的GJK实现是 d tripleProduct(ab, ao, ab); d.set(this.tripleProduct(ab, ao, ab)); } else { // 原点在ab的“右侧”或线上退化到点a simplex.splice(1, 1); // 移除b d.set(ao); } return false; } private static processTriangle(simplex: MinkowskiPoint[], d: Vec2): boolean { let a simplex[0].point; let b simplex[1].point; let c simplex[2].point; let ab b.subtract(a); let ac c.subtract(a); let ao new Vec2(-a.x, -a.y); // 检查边AB let abPerp new Vec2(-ab.y, ab.x); if (abPerp.dot(ao) 0) { // 原点在AB外侧移除C退化到线段AB simplex.splice(2, 1); d.set(this.tripleProduct(ab, ao, ab)); return false; } // 检查边AC let acPerp new Vec2(-ac.y, ac.x); if (acPerp.dot(ao) 0) { // 原点在AC外侧移除B退化到线段AC simplex.splice(1, 1); d.set(this.tripleProduct(ac, ao, ac)); return false; } // 原点在三角形ABC内 return true; } // 三重积辅助函数 (a × b) × c 在2D中的计算 // 返回一个垂直于a的向量方向由b和c决定。常用形式 (a · c) * b - (a · b) * c private static tripleProduct(a: Vec2, b: Vec2, c: Vec2): Vec2 { let ac a.dot(c); let ab a.dot(b); // result ac * b - ab * c let result new Vec2(); Vec2.multiplyScalar(b, ac, result); let temp new Vec2(); Vec2.multiplyScalar(c, ab, temp); return result.subtract(temp); } // 计算垂直向量 private static perp(v: Vec2): Vec2 { return new Vec2(-v.y, v.x); } }4.2 在Cocos Creator场景中的测试组件创建一个新的组件脚本GJKTestComponent.ts并挂载到一个空节点上。import { _decorator, Component, Vec2, Color, Graphics, Node } from cc; import { Shape, GJKCollisionDetector } from ./GJKCollision; const { ccclass, property } _decorator; ccclass(GJKTestComponent) export class GJKTestComponent extends Component { property(Node) shapeANode: Node null!; property(Vec2) shapeAVerticesLocal: Vec2[] [new Vec2(-50, -50), new Vec2(50, -50), new Vec2(0, 50)]; property(Node) shapeBNode: Node null!; property(Vec2) shapeBVerticesLocal: Vec2[] [new Vec2(-30, -30), new Vec2(30, -30), new Vec2(30, 30), new Vec2(-30, 30)]; private _graphics: Graphics null!; start() { // 获取或创建Graphics组件用于绘制 this._graphics this.getComponent(Graphics) || this.addComponent(Graphics); this._graphics.lineWidth 2; } update() { if (!this.shapeANode || !this.shapeBNode) return; let worldVertsA this.transformVertices(this.shapeAVerticesLocal, this.shapeANode); let worldVertsB this.transformVertices(this.shapeBVerticesLocal, this.shapeBNode); let shapeA new Shape(worldVertsA); let shapeB new Shape(worldVertsB); let isColliding GJKCollisionDetector.checkCollision(shapeA, shapeB); // 可以在控制台输出或更新UI文本 // console.log(Collision: ${isColliding}); this.drawShapes(isColliding); } private transformVertices(localVerts: Vec2[], node: Node): Vec2[] { let worldVerts: Vec2[] []; for (let localVert of localVerts) { let worldPos new Vec2(); // 将本地坐标相对于节点转换到世界坐标这里简化处理假设节点没有旋转缩放只有位移 // 实际项目中应考虑节点的完整变换矩阵 worldPos.x node.worldPosition.x localVert.x; worldPos.y node.worldPosition.y localVert.y; worldVerts.push(worldPos); } return worldVerts; } private drawShapes(isColliding: boolean) { this._graphics.clear(); // 绘制形状A this._graphics.strokeColor Color.GREEN; this.drawShape(this.shapeAVerticesLocal, this.shapeANode); // 绘制形状B this._graphics.strokeColor Color.BLUE; this.drawShape(this.shapeBVerticesLocal, this.shapeBNode); // 在碰撞时绘制一个红色的X if (isColliding) { this._graphics.strokeColor Color.RED; this._graphics.moveTo(-10, -10); this._graphics.lineTo(10, 10); this._graphics.moveTo(-10, 10); this._graphics.lineTo(10, -10); this._graphics.stroke(); } } private drawShape(localVerts: Vec2[], node: Node) { if (localVerts.length 0) return; let firstWorldPos this.localToWorld(localVerts[0], node); this._graphics.moveTo(firstWorldPos.x, firstWorldPos.y); for (let i 1; i localVerts.length; i) { let worldPos this.localToWorld(localVerts[i], node); this._graphics.lineTo(worldPos.x, worldPos.y); } // 闭合图形 this._graphics.lineTo(firstWorldPos.x, firstWorldPos.y); this._graphics.stroke(); } private localToWorld(localVert: Vec2, node: Node): Vec2 { return new Vec2(node.worldPosition.x localVert.x, node.worldPosition.y localVert.y); } }在Cocos Creator编辑器中创建两个空节点作为shapeANode和shapeBNode并将GJKTestComponent脚本挂载到画布或一个管理节点上将这两个空节点拖拽赋值。运行后你就能看到两个形状的轮廓当它们重叠时屏幕中心会绘制一个红色的X表示碰撞。5. 关键细节、优化与常见问题排查实现基础GJK只是第一步。要让它在实际项目中可用还需要处理很多细节。5.1 穿透向量EPA与碰撞响应GJK只能告诉你“是否碰撞”。对于物理响应如物体弹开我们还需要知道“碰撞深度”和“碰撞法线”即一个将物体推出重叠区域的最小平移向量MTV。这需要另一个算法EPAExpanding Polytope Algorithm。EPA在GJK找到碰撞单纯形包含原点后以该单纯形为起点不断扩展多边形直到找到A - B凸包上离原点最近的边该边的法线方向即碰撞法线原点到该边的距离即穿透深度。实操心得对于简单的游戏如果不需要非常精确的物理响应有时可以用GJK迭代过程中得到的最后一个单纯形靠近原点的边来近似估算碰撞法线和深度虽然不精确但计算量小。对于需要精确物理的场景必须实现EPA。5.2 性能优化要点缓存支撑点对于旋转/缩放不变的静态或运动缓慢的物体可以缓存其世界空间顶点避免每帧都进行坐标变换。快速拒绝在执行GJK前先进行粗略的碰撞检测如轴对齐包围盒AABB或包围球检测。这可以快速过滤掉明显不相交的物体对节省大量计算。方向缓存GJK迭代中上一次的搜索方向d对于下一帧的检测通常是一个很好的初始方向因为物体运动具有连贯性。可以在物体对之间缓存这个方向。顶点数优化对于复杂凸多边形可以使用凸包算法如Graham Scan先简化顶点或者使用预先计算好的凸包近似如8边形或12边形来代替高精度模型进行碰撞检测。5.3 常见问题与调试技巧算法陷入无限循环或提前退出原因通常是由于浮点数精度误差导致的方向计算错误或者TripleProduct函数实现有误产生了零向量或方向错误。排查在迭代中打印出每一步的单纯形顶点和搜索方向d。确保d不为零向量。可以给d一个极小的最小值如(1e-10, 1e-10)来避免除零或方向退化。修复在ProcessLine和ProcessTriangle中当计算出的新方向d长度接近0时可以尝试使用一个备选方向比如垂直于当前边的法线或者直接使用原点指向最近点的向量。检测结果不稳定闪烁原因当两个物体刚好相切或非常接近时浮点误差可能导致一帧检测为碰撞下一帧检测为分离。解决引入一个微小的“容差”epsilon。例如在判断点积if (Vector2.Dot(newPoint.point, d) 0)时可以改为if (Vector2.Dot(newPoint.point, d) epsilon)其中epsilon是一个很小的正数如1e-6。这相当于给碰撞检测增加了一个微小的“皮肤”使结果更稳定。对于凹多边形无效核心限制GJK算法只适用于凸体。如果你的形状是凹的必须将其分解为多个凸体的组合凸分解然后对每一对凸体分别进行GJK检测。工具可以使用诸如Bayazit分解等算法进行自动凸分解或者在建模时直接使用凸体碰撞器。坐标变换错误现象形状在屏幕上显示正确但碰撞检测区域错位。检查确保传递给GJK算法的顶点坐标是在同一个坐标系通常是世界坐标系下的。仔细检查TransformVertices函数确保它正确应用了物体的位置、旋转和缩放。在Cocos Creator示例中我们简化了变换实际项目应使用节点的worldMatrix进行完整变换。EPA实现中的数值稳定性难点EPA在寻找最近边时需要计算点到线的距离并处理可能出现的退化三角形面积接近零。技巧在计算距离时使用叉积的绝对值除以边长避免除零。对于面积过小的三角形直接舍弃或进行特殊处理。同样引入容差来应对浮点误差。通过亲手实现GJK你获得的不只是一个碰撞检测工具更是对物理引擎底层逻辑的深刻理解。这份理解能让你在遇到Unity或Cocos Creator内置物理引擎的怪异行为时不再束手无策而是有能力去分析、验证甚至定制自己的解决方案。记住所有复杂的系统都是由这些基础的算法构建而成的掌握了基础你就拥有了拆解复杂问题的能力。