ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

连续碰撞检测(Continuous Collision Detection, CCD)

连续碰撞检测(Continuous Collision Detection, CCD) 穿透是怎么发生的物理引擎不是连续运行的它每隔一帧比如 1/60 秒才检查一次碰撞。这中间物体在瞬移上一帧在墙左边下一帧在墙右边引擎看到的两张快照都没碰到墙于是判定没碰撞。一句话的判断标准物体一帧走的距离比障碍物的厚度还大就有漏检风险。子弹一帧走 13 米墙厚 0.2 米 —— 必穿。角色下落一帧走 0.4 米单向平台厚 0.1 米 —— 必穿。先分清两种问题它们看起来都叫穿透但原因和解法完全不同。一种是嵌进去了两个物体重叠在一起引擎看得见只是需要把它们推开。常见于角色靠墙、箱子堆叠。另一种是穿过去了引擎从头到尾没看见碰撞。常见于高速弹丸、快速下落。把第二种当第一种处理是最常见的 bug 来源 —— 因为穿过去之后引擎算出来的推开方向经常是错的会把角色弹到墙的另一面。解法一补帧子步最直白的思路既然采样太稀疏那就多采样几次。把一帧切成 4 份每份单独做一次完整的碰撞检测。publicstaticintRequiredSubsteps(floatspeed,floatdt,floatthinnestWall,intcap8){intn(int)MathF.Ceiling(speed*dt/thinnestWall);returnMath.Clamp(n,1,cap);}优点是改动小什么形状都能用。缺点是成本线性增长。前面那颗子弹需要 67 个子步才够没人付得起这个代价所以子步只适合快但不是特别快的东西比如赛车。解法二扫掠算出碰撞发生的时刻换个思路不要只看起点和终点而是把这一帧的运动轨迹当成一条线段直接算出它和障碍物相交的位置。这就是射线检测只不过考虑了物体的体积。对球、胶囊这些简单形状这件事有现成的闭式解一次计算就能得到碰撞发生在这一帧的 37% 处。/// 球撞平面看距离什么时候归零publicstaticboolSweepCircleVsPlane(Vector2center,floatradius,Vector2delta,Vector2normal,floatplaneOffset,outfloattoi){toi0f;floatgapVector2.Dot(normal,center)-planeOffset-radius;// 当前离墙面多远floatclosingVector2.Dot(normal,delta);// 这一帧靠近多少if(closing0f)returnfalse;// 没在靠近if(gap0f)returntrue;// 已经嵌进去了走另一套流程floattgap/-closing;// 要走掉 gap需要这一帧的几成if(t1f)returnfalse;// 这一帧还到不了toit;returntrue;}gap / closing就是全部原理还剩多少距离除以这一帧能走多少得到百分比。拿到这个百分比之后把物体放到碰撞那一刻的位置再处理反弹或停止。这是高速物体的标准解法成本和速度无关子弹再快也是一次计算。形状复杂到没有闭式解时两个凸包互撞用迭代逼近测一下当前最近距离 d如果相对速度是 v那么 d/v 这段时间内绝对不可能碰上直接跳过去再测一次。每一步都安全不会跨过碰撞时刻。解法三提前刹车投机接触前两种都在碰撞已经可能发生之后补救。还有一种更顺的做法在还没碰到的时候就建立约束。规则很简单 —— 这一帧允许靠近的距离不能超过现在的间隙。离墙还有 3 厘米这一帧最多只许走 3 厘米。publicstaticvoidSolveVelocity(refBodya,refBodyb,Vector2n,floatgap,floatrestitution,floatdt){floatinvSuma.InvMassb.InvMass;if(invSum0f)return;floatclosingVector2.Dot(a.Velocity-b.Velocity,n);floatbudgetgap0f?gap/dt:0f;// 这一帧允许的接近速度if(closingbudget0f)return;// 够不着不管floategap0f?0f:restitution;// 还有间隙时不要加弹性floatj-(1fe)*(closingbudget)/invSum;a.Velocityn*(j*a.InvMass);b.Velocity-n*(j*b.InvMass);}好处是物体永远不会先陷进去再被推出来所以画面上完全不抖也不会穿过去。适合小而快、手感要求高的东西。解法四已经嵌进去了怎么推开推开本身有两个反直觉的细节。第一不要一次推到位。全量修正会给系统注入能量物体会弹起来。标准做法是每帧只修正 20%几帧之内收敛。第二要留一点容差通常 5 毫米允许残余的轻微重叠。这一条是堆叠稳定的关键如果要求刚好接触箱子会被推到零点、重力又让它陷下去一点、再被推上来永远在亚毫米级抖动。留出容差系统就有了一个可以休息的区间。constfloatSlop0.005f;// 允许的残余重叠constfloatBeta0.2f;// 每帧修正比例constfloatMaxCorrection0.2f;// 单帧上限防止深穿透弹飞publicstaticvoidPositionalCorrection(refBodya,refBodyb,Vector2n,floatdepth){floatinvSuma.InvMassb.InvMass;if(invSum0f)return;// 两个都是静态体floatdMathF.Min(MathF.Max(depth-Slop,0f),MaxCorrection);Vector2pushn*(d*Beta/invSum);a.Positionpush*a.InvMass;// 轻的推得多静态体不动b.Position-push*b.InvMass;}InvMass是质量的倒数静态物体设为 0于是它一动不动全部位移都给了另一个物体。这一行就实现了角色撞墙是角色被挡住而不是墙被推开。游戏案例子弹穿墙7.62mm 弹丸 800 m/s一帧走 13 米。子步要 67 个不可行。业界的两条路hitscan 武器CS、Valorant直接发一条射线一次求交就出结果弹道模拟战地、PUBG每帧把弹丸这一段轨迹当胶囊做一次扫掠。重力造成的抛物线按帧切成直线段处理误差在毫米级看不出来。角色掉出地板角色下落 25 m/s一帧 0.42 米单向平台厚度一般只有 0.1 米必漏检。这里特别要注意不要用常规的重叠推开。角色下落快的时候已经穿到平台下面去了引擎算出来的最短推开方向会变成水平的角色会被横向弹飞。正确做法是看脚底相对平台的上下关系publicstaticboolLandOnPlatform(Vector2prevFoot,Vector2foot,floatplatformY,floatxMin,floatxMax,outfloattoi){toi0f;floataboveprevFoot.Y-platformY;// 上一帧在上方为正floatnowfoot.Y-platformY;// 这一帧if(above0f||now0f)returnfalse;// 不是从上往下穿过toiabove/(above-now);// 穿过发生在这一帧的几成floatxprevFoot.X(foot.X-prevFoot.X)*toi;returnxxMinxxMax;// 水平范围内才算踩到}上一帧在上面这一帧在下面就是穿越的充分判据完全绕开了方向判断出错的问题。单向平台从下往上能穿过去的特性也顺便由第一个条件实现了。赛车撞护栏250 km/h ≈ 70 m/s一帧 1.17 米护栏厚 0.3 米算下来 4 个子步就够。车体是凸包不是球没有现成的闭式解所以子步在这里反而比扫掠划算。常见配置是 4 到 8 个子步再用迭代逼近兜底。弹球机球小、速度高、挡板自己还在高速翻转。等重叠了再推开球会卡进挡板或者被弹出离谱的速度。这是投机接触最适合的场景球每次都正好停在挡板表面反弹方向稳定可预测。箱子堆一直在抖这不是穿透是容差没配。把 Slop 设成 0 就会出现这个症状。另一个相关现象是把修正比例从 0.2 调到 1.0抖动会变成肉眼可见的弹跳 —— 因为位置修正在凭空注入能量而速度求解并不知道这件事。
返回列表