ARTICLE DETAIL

资讯详情

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

深度解析KCC:角色移动系统的核心算法

深度解析KCC:角色移动系统的核心算法 前面几篇一直在提角色移动代码、CollideAndSlide但都是一带而过。这篇把 KCCKinematic Character Controller运动学角色控制器的运动求解核心拆开讲清楚——它是整个角色移动系统的心脏也是浮点确定性、卡墙、穿模这些问题的集中爆发地。先分清 KCC 和物理角色的区别角色控制器大体两条路线物理驱动Dynamic给角色一个刚体靠施加力、速度让物理引擎去解。好处是和物理世界交互自然坏处是手感难调、容易被推飞、确定性差。运动学驱动Kinematic也就是 KCC角色不受物理引擎的力控制位置完全由我们自己算。我们每帧算出这一帧想移动多少然后自己做碰撞检测、自己决定最终落点。物理引擎只提供查询能力射线、扫掠、重叠检测不替我们做决策。FPS、大部分第三人称动作游戏用的都是 KCC因为手感完全可控。而自己算位置这件事的核心就是运动求解。运动求解要回答的问题一句话概括 KCC 每帧要解的问题我想从position移动motion这么多但路上有墙、有斜坡、有台阶。请问我最终应该停在哪以及撞墙之后剩下的动量该往哪去拆开就是三个子问题Collide and Slide撞到障碍后沿着表面滑走而不是直接停死。Depenetration解穿透如果角色已经陷进了几何体里怎么推出来。Ground / Step / Slope地面检测、上下台阶、斜坡上下坡的处理。这三块合起来就是运动求解核心。我们逐个讲。一、Collide and Slide核心中的核心朴素的移动是position motion直接穿墙。KCC 的做法是把这一步换成扫掠 滑动的迭代。基本思路把想走的motion沿方向做一次扫掠检测capsule sweep用胶囊体沿运动方向扫过去看撞不撞得到东西。撞到了就走到接触点前一点点然后把剩余的运动量投影到碰撞平面上沿着平面继续走。这样一次可能不够比如墙角滑一次又撞到另一面墙所以要迭代几次。Vector3CollideAndSlide(Vector3 position,Vector3 motion,intmaxIterations4){Vector3 remainingmotion;for(inti0;imaxIterations;i){floatdistanceremaining.magnitude;if(distanceEPSILON)break;// 没剩多少要走的了Vector3 dirremaining/distance;HitResult hit;// 用胶囊体沿 dir 扫掠 distance加一点 skin width 余量if(!CapsuleSweep(position,dir,distanceSKIN_WIDTH,out hit)){// 一路畅通直接走完positionremaining;break;}// 撞到了走到接触点前 SKIN_WIDTH 的位置别贴死floatsafeDistmax(hit.distance-SKIN_WIDTH,0);positiondir*safeDist;// 关键一步把剩余运动投影到碰撞平面上Vector3 leftoverremaining-dir*safeDist;remainingProjectOnPlane(leftover,hit.normal);}returnposition;}// 把向量投影到以 normal 为法线的平面上也就是去掉法线方向的分量Vector3ProjectOnPlane(Vector3 v,Vector3 normal){returnv-normal*Dot(v,normal);}有几个点必须讲透Skin Width皮肤厚度。你注意到那个SKIN_WIDTH了吗。永远不要让角色胶囊体和墙面严丝合缝地贴上去——浮点误差会让刚好接触变成陷进去一点点下一帧就要解穿透来回横跳就是抖动。留一层薄薄的皮肤比如 0.01~0.02 米永远停在接触点前面一点是稳定性的关键。这也是前面那篇讲浮点时提到的稳定性技巧的来源。迭代次数。为什么要迭代想象你冲进一个墙角两面墙夹 90 度。第一次扫掠撞到左墙滑动后沿墙走但滑动方向又撞到右墙需要第二次扫掠再投影。一般 3~4 次迭代足够处理绝大多数几何。次数太少会卡角太多是浪费。投影是灵魂。ProjectOnPlane决定了撞墙不停死而是擦着墙滑走这个手感。它做的事就是把速度里撞进墙的那个分量去掉只留下沿着墙的分量。二、多平面约束滑动的进阶坑上面的朴素投影有个经典 bug在两面墙夹角处如果单纯地一次次投影可能会把角色又推回第一面墙里。具体说撞左墙投影到左墙平面再撞右墙投影到右墙平面——但这次投影可能又让运动方向指回了左墙内部。结果就是在夹角里反复横跳或者被卡死。工业级的做法是记录所有接触平面让最终运动同时满足所有约束不能穿进任何一个已接触的平面。这就是 Quake 那套经典的ClipVelocity 多平面处理的思路很多引擎沿用至今。// 简化版多平面约束碰到第二个平面时沿两平面的交线走Vector3HandleTwoPlanes(Vector3 velocity,Vector3 n1,Vector3 n2){// 两个平面的交线方向 两法线的叉积Vector3 creaseCross(n1,n2).normalized;// 把速度投影到交线上这样既不进 n1 也不进 n2returncrease*Dot(velocity,crease);}这个 case 在实际项目里非常容易出问题——玩家卡在门框、卡在两个箱子中间动不了八成是多平面约束没处理好。三、Depenetration陷进去了怎么办理论上有了 skin width 和正确的滑动角色不该陷进几何体。但现实里总会发生平台移动/旋转把角色挤进了墙里场景动态生成、传送、复活点没放好浮点误差累积到突破了 skin width所以每帧求解开始前通常先做一次重叠检测 解穿透查询角色胶囊体和哪些碰撞体重叠了算出最小分离向量MTVMinimum Translation Vector把角色推出来。Vector3ResolvePenetration(Vector3 position){// 查出所有和角色胶囊重叠的碰撞体var overlapsComputePenetration(position);foreach(var o in overlaps){// o.direction 是推出方向o.distance 是需要推出的深度positiono.direction*o.distance;}returnposition;}注意解穿透要在移动求解之前做先保证起点是干净的再谈往哪走。否则你从一个陷进去的位置开始扫掠结果全是错的。四、地面、斜坡、台阶手感的分水岭这部分决定了这个角色控制器好不好用是 KCC 里最需要打磨的地方。地面检测Grounding。每帧向下做一次短距离扫掠判断脚下有没有地面、地面法线是什么。这个结果决定了能不能跳、要不要贴地、算不算在斜坡上。地面检测的稳定性直接影响跳跃手感和是否在空中的判定。斜坡Slope。站在斜坡上重力会让你往下滑。要不要滑、滑多快取决于坡度floatslopeAngleAngle(groundNormal,Vector3.up);if(slopeAnglemaxSlopeAngle){// 可站立的坡把移动投影到斜面上让角色贴着坡走而不是往坡里钻或者飘起来motionProjectOnPlane(motion,groundNormal);}else{// 太陡了站不住强制沿坡下滑motionSlideDownSlope(groundNormal);}在斜坡上走路特别要注意如果不把水平移动投影到斜面上角色要么钻进坡里然后被解穿透弹出来抖要么水平飞出去再掉下来一跳一跳的。投影到斜面是让上下坡丝滑的关键。台阶Step。玩家希望能自动迈上矮台阶楼梯、马路牙子而不是被 5 厘米的坎卡住。做法是当水平移动被挡住时尝试抬高一点 → 往前走 → 落下来这个组合动作如果落点是有效地面且高度差在maxStepHeight之内就允许上去。// 台阶检测的思路简化boolTryStepUp(ref Vector3 position,Vector3 moveDir){// 1. 先假装抬高 maxStepHeightVector3 raisedpositionVector3.up*maxStepHeight;// 2. 在抬高的位置往前走if(CapsuleSweep(raised,moveDir,stepForwardDist,out _))returnfalse;raisedmoveDir*stepForwardDist;// 3. 往下落看有没有落到台阶面上if(CapsuleSweep(raised,Vector3.down,maxStepHeight,out var ground)){positionraisedVector3.down*ground.distance;returntrue;// 成功上台阶}returnfalse;}台阶处理最容易出的问题是上台阶时的瞬间跳变——玩家会看到相机猛地一顿。所以逻辑上位置可以瞬间抬升但渲染/相机要平滑过渡这又回到了前面讲的逻辑位置和渲染位置分离。把它们串起来一帧的完整求解流程voidSolveMovement(floatdt){// 0. 先把陷进去的推出来保证起点干净positionResolvePenetration(position);// 1. 检测地面得到 isGrounded 和 groundNormalGroundInfo groundProbeGround(position);// 2. 根据输入和地面状态更新速度含重力、摩擦、加速度velocityUpdateVelocity(velocity,input,ground,dt);// 3. 把速度投影到地面/斜面上如果在地面Vector3 motionvelocity*dt;if(ground.isGrounded)motionProjectOnPlane(motion,ground.normal);// 4. 核心带滑动和多平面约束的碰撞求解positionCollideAndSlide(position,motion);// 5. 尝试上下台阶HandleSteps(ref position,motion);// 6. 贴地在地面上时向下吸附避免下坡时飘在空中if(ground.isGrounded)positionSnapToGround(position);}这就是 KCC 运动求解核心的骨架。真实项目里每一步都有大量细节要打磨但主干就是这个顺序解穿透 → 探地 → 更新速度 → 投影 → 滑动求解 → 台阶 → 贴地。回到确定性这条主线讲了这么多把它和前面几篇串起来KCC 的运动求解里ProjectOnPlane的点积、CapsuleSweep的距离计算、normalize的sqrt、斜坡角度的Angle——全是浮点运算全是确定性问题的高发区。这也是为什么前面几篇反复围着这段代码打转。如果你的项目要跨端一致那么这套求解逻辑必须做成客户端/服务器共享的纯计算模块前面 FPS 篇讲的第二刀。里面所有浮点运算受约束浮点或软件浮点约束上一篇讲的路线。SKIN_WIDTH、maxSlopeAngle、maxStepHeight这些阈值两端必须完全一致。迭代次数、遍历顺序都要确定,不能依赖平台相关的行为。小结KCC 运动求解核心本质就是回答想走这么多、但路上有障碍最终停哪、动量往哪去。它由三大块构成Collide and Slide——撞墙滑走的迭代求解灵魂是把运动投影到碰撞平面难点是墙角的多平面约束。Depenetration——每帧先把陷进几何体的角色推干净再谈移动。Ground / Slope / Step——地面探测、斜坡投影、台阶跨越决定了手感好不好。这段代码既是手感的来源也是浮点确定性问题的震中。把它写稳、写成可复用的纯逻辑模块前面几篇讲的所有确定性方案才有落地的地方。
返回列表