
1. 从一次角色穿模说起物理与动画系统为什么是引擎的任督二脉做过游戏的人大概都遇到过这种场景角色跑着跑着脚陷进了地面或者被击飞之后身体像面条一样穿过墙壁再或者明明动画播的是落地翻滚结果人却悬在半空滑行。这些看起来是美术问题、逻辑问题的bug追根溯源十有八九都落在物理系统和动画系统的交界处。物理系统和动画系统是游戏引擎里最容易被低估、也最容易出问题的两个模块。渲染差了玩家顶多觉得画面糙但物理和动画一旦出问题玩家会直接觉得这游戏手感稀烂。而这两个系统恰恰又是耦合最深的——动画驱动骨骼骨骼带着碰撞体走碰撞体又反过来约束动画的播放状态。它们之间的数据流一旦理不顺穿模、抖动、滑步、卡顿就会接踵而至。这篇内容我打算把物理系统和动画系统拆开揉碎讲一遍重点不是罗列API而是讲清楚它们各自内部是怎么运转的、两者之间是怎么交接的、以及在实际项目里哪些地方最容易翻车。不管你是刚接触引擎架构的新人还是已经写过几年 gameplay 逻辑、想往底层再挖一层的开发者应该都能从中找到对自己有用的部分。我会尽量用生活化的类比把抽象机制讲明白同时把关键的数据结构、更新顺序、参数含义都落到实操层面。需要先说明一点不同引擎Unity、Unreal、Godot、自研引擎的具体实现差异很大但核心架构思想是相通的。下面讲的内容以通用架构为主涉及具体引擎时会点明差异你对照自己用的引擎去理解即可。2. 物理系统的三层结构碰撞检测、约束求解、场景查询很多人一提到物理引擎脑子里第一反应就是牛顿定律重力加速度。但实际上游戏物理引擎和真实物理模拟是两回事。真实物理追求的是精确游戏物理追求的是稳定、可控、够快。所以游戏物理引擎的内部结构本质上是围绕如何在有限算力下让一堆刚体看起来合理地动起来来设计的。2.1 碰撞检测宽相与窄相的分工逻辑物理系统最底层、最耗性能的部分就是碰撞检测。如果场景里有1000个物体两两检测就是接近50万次配对这在实时帧率下根本跑不动。所以工业级物理引擎都会把碰撞检测拆成两个阶段宽相Broad Phase和窄相Narrow Phase。宽相的思路很朴素先用便宜的、粗略的方法把明显不可能碰撞的物体对排除掉只留下有可能碰撞的候选对。常用的做法是空间划分比如动态AABB树BVH、均匀网格、扫描-剪枝Sweep and Prune。你可以把它想象成快递分拣——先按城市分大类不可能同城的包裹根本不用细看。窄相才是真正做精确几何相交测试的地方。它拿到宽相筛出来的候选对用GJK、SAT分离轴定理、EPA这些算法去算两个形状到底有没有重叠、重叠多深、法线朝哪。这里有个关键输出叫接触流形Contact Manifold它不只是记录碰了还要记录接触点、接触法线、穿透深度。这些数据直接决定后面约束求解的质量。提示宽相的性能瓶颈往往不在算法本身而在物体频繁移动导致的树结构重建。如果你的场景里有大量高速移动物体宽相的更新开销会飙升这是很多项目在中后期才暴露的性能坑。2.2 约束求解为什么物理引擎需要迭代检测到碰撞之后物理引擎要做的是把穿进去的物体推出来同时让它们按照合理的速度反弹或滑动。这件事听起来简单做起来极其麻烦因为一个物体可能同时和多个物体接触每个接触都提出一个约束这些约束之间会互相打架。物理引擎的解法是迭代求解。最经典的是顺序冲量法Sequential Impulse把所有约束排成一队一个一个轮流满足满足完一轮再从头来一遍迭代若干次之后整体误差会收敛到一个可接受的范围。这就像一群人挤在电梯里每个人都在调整站位来回挪几次之后大家就都站得下了。这里有个非常重要的参数叫迭代次数Solver Iterations。迭代次数越高物理越稳定但CPU开销越大。很多项目默认用8到10次遇到堆叠场景比如一堆箱子摞在一起就容易抖这时候把迭代次数提到20甚至更高抖动会明显改善。但代价是性能所以实际项目里通常配合休眠机制Sleeping一起用——静止的物体直接标记为睡眠不参与求解。2.3 场景查询射线检测与形状投射的底层除了模拟物体运动物理系统还对外提供场景查询能力最典型的就是射线检测Raycast和形状投射Shape Cast。玩家点击屏幕选中一个物体、子弹判断有没有打中敌人、AI判断前方有没有障碍靠的都是这套接口。射线检测的本质是从起点沿方向发射一条线问它第一个撞到谁。听起来简单但实现上要遍历空间结构效率差异很大。这里有个实操经验射线检测的起点和终点如果都在物体内部很多引擎会返回失败因为它检测的是进入而不是离开。做近战判定的时候如果攻击起点设在角色碰撞体内部就容易漏判正确做法是把起点稍微往前推一点或者用重叠检测Overlap代替。形状投射则是把线换成体比如胶囊体投射用来做角色移动的防穿墙检测。它的开销比射线大但能避免点检测带来的漏判问题。3. 动画系统的数据流从骨骼到蒙皮到底经历了什么物理系统管的是物体怎么动动画系统管的是角色怎么动。这两件事看起来像是一回事实际上在引擎里是完全独立的两套管线。理解动画系统关键是理解一帧动画数据是怎么从美术做的资源变成屏幕上会动的模型的。3.1 骨骼动画的本质矩阵的层级传递骨骼动画的核心是层级变换。美术在DCC工具Maya、Blender里绑好骨骼每根骨骼相对父骨骼有一个局部变换Local Transform这个变换由位移、旋转、缩放组成。引擎在运行时从根骨骼开始一层层把局部变换乘起来得到每根骨骼的世界变换World Transform。这个过程叫骨骼层级求值是动画系统每帧都要做的事。它的计算量正比于骨骼数量一个角色几十根骨骼还好但同屏几十个角色、每个角色上百根骨骼开销就很可观了。所以引擎通常会做脏标记优化只有变换发生变化的骨骼才重新求值没变的直接复用上一帧结果。这里有个容易踩的坑缩放Scale在骨骼动画里要慎用。因为缩放会破坏旋转的正交性如果父骨骼有非均匀缩放子骨骼的旋转会被剪切变形出现诡异的扭曲。绝大多数项目里骨骼的缩放都保持为1需要整体放大角色就缩放根节点。3.2 动画混合为什么角色动作能平滑过渡如果每个动作都是独立播放角色从走切到跑就会瞬间跳变非常生硬。动画系统的解法是混合Blending。最基础的是线性混合两个动画的姿势按权重插值权重从0到1过渡动作就平滑了。但线性混合有个问题它是对每个骨骼的变换分别插值如果两个动作差异很大比如站立和趴下中间帧会出现不自然的姿势。所以进阶方案是骨骼遮罩Avatar Mask配合分层混合——上半身播射击、下半身播跑步两层各管各的骨骼互不干扰。再往上还有加法动画Additive Animation用来做在基础动作上叠加偏移比如角色一边跑一边被风吹得身体倾斜。加法动画存的是相对于参考姿势的偏移量叠加时直接加到基础姿势上非常适合做受击、呼吸、瞄准这类局部微调。3.3 状态机与动画图谁来决定播哪个动作动画资源准备好了混合规则也定了接下来就是什么时候播哪个。这就是动画状态机Animation State Machine的职责。它本质上是一张有向图节点是动画状态边是转移条件。角色从待机到移动靠的是速度参数超过阈值触发转移。状态机的好处是直观策划能看懂改起来方便。但它的缺点是状态爆炸——角色有移动、跳跃、攻击、受击、死亡每个状态又分方向、分武器状态数量会指数级增长。所以现代引擎更推荐动画图Animation Graph或者混合空间Blend Space的思路用参数空间来连续控制动画而不是靠离散状态跳转。混合空间最典型的用法是二维移动混合横轴是左右速度纵轴是前后速度角色朝哪个方向移动就在这个二维空间里取对应位置的动画混合结果。这样八个方向的移动只需要一个混合空间不用做八个状态。4. 物理与动画的交接点为什么穿模和滑步总在这里发生前面两章分别讲了物理和动画但真正让开发者头疼的是这两个系统的交界处。角色移动这件事既涉及动画腿在动又涉及物理身体在移动两者一旦不同步就会出现滑步、穿模、抖动。4.1 根运动让动画驱动位移还是让代码驱动位移角色移动有两种主流做法。第一种是代码驱动逻辑层算出速度直接改角色位置动画只负责看起来在跑。第二种是根运动Root Motion动画本身带有位移信息引擎从动画里提取根骨骼的位移应用到角色上。代码驱动的优点是可控网络同步简单但缺点是容易滑步——如果动画的步频和实际移动速度不匹配脚就会在地上打滑。根运动的优点是动作和位移天然同步但缺点是网络同步麻烦而且动画一旦被混合或打断位移就会出问题。实际项目里最常见的方案是混合使用平时用代码驱动特定动作比如翻滚、处决、攀爬用根运动。这里的关键是根运动的提取时机——必须在动画求值之后、物理更新之前提取否则会用到上一帧的旧数据导致位移延迟一帧表现为轻微抖动。4.2 碰撞体与骨骼的绑定胶囊体为什么是角色标配角色的视觉表现是骨骼蒙皮但物理碰撞用的是简化的碰撞体。绝大多数游戏里角色用的是胶囊体Capsule。为什么是胶囊而不是盒子或者球因为胶囊在斜坡上滑动平滑、不会卡在台阶边缘、旋转时形状不变而且胶囊与胶囊的碰撞检测非常快。胶囊体通常绑定在角色的根骨骼或者一个专门的物理根节点上位置和朝向跟随角色逻辑位置但不跟随动画。也就是说角色做翻滚动作时视觉上身体在翻但物理胶囊体始终是竖直的。这是刻意的设计——如果碰撞体跟着动画翻物理表现会变得极其不稳定。注意有些项目为了追求精确打击给角色四肢也加了碰撞体。这在格斗游戏里可行但在大规模场景里性能开销很大而且容易产生自碰撞自己的手打到自己的胸。如果非要做记得用碰撞层Collision Layer把自身骨骼之间的碰撞关掉。4.3 动画通知与物理事件的时序问题动画系统有个很实用的机制叫动画通知Animation Notify可以在动画的特定时间点触发回调比如脚落地的那一刻播放音效挥刀到一半时开启攻击判定。这个机制和物理系统的配合非常微妙。问题出在时序上。动画通知是在动画求值阶段触发的而物理更新在另一个阶段。如果通知里直接改物理状态比如给角色施加一个冲量而这个冲量在当帧的物理更新里没被处理就会延迟到下一帧表现为打击感发飘。正确的做法是动画通知只负责标记事件把事件塞进一个队列等物理更新阶段统一处理。这样能保证事件和物理在同一帧内生效打击感才扎实。这个细节很多教程不会讲但它是动作游戏手感的关键之一。5. 更新顺序一帧之内物理和动画到底谁先谁后前面反复提到时序这一章专门把一帧内的更新顺序讲透。这是引擎架构里最容易被忽视、但影响最深远的设计决策之一。5.1 典型的主循环阶段划分一个成熟的游戏引擎一帧通常会划分成这么几个阶段输入采集读取玩家输入更新输入状态。逻辑更新Gameplay跑游戏逻辑状态机、AI、技能系统。动画更新求值骨骼层级、混合动画、触发动画通知。物理更新碰撞检测、约束求解、场景查询。变换同步把物理结果写回渲染节点。渲染提交绘制命令。这个顺序不是随便定的。动画在物理之前是因为动画通知可能要影响物理比如开启攻击判定物理在渲染之前是因为渲染要用最新的位置。但这里有个矛盾动画求值需要角色的位置而位置可能被物理改变。5.2 固定步长与可变步长的取舍物理更新通常用固定步长Fixed Timestep比如每秒60次不管帧率多少物理都按固定间隔推进。这样做是为了确定性——同样的输入物理结果应该一致这对回放、网络同步、物理稳定性都至关重要。动画更新则通常用可变步长跟着渲染帧率走。因为动画是看起来对就行不需要严格确定性。但这就带来一个问题如果渲染帧率是120物理是60那么有些渲染帧里物理没更新动画却更新了两者就会不同步。引擎的解法是插值物理更新后保存前后两个状态渲染时根据当前时间在两个状态之间插值。这样即使物理没更新渲染出来的位置也是平滑的。这个机制叫状态插值State Interpolation是保证高帧率下物理表现平滑的关键。5.3 一个真实的时序bug复盘我遇到过这样一个问题角色在斜坡上移动时偶尔会突然弹飞。排查了很久最后定位到时序上。原因是动画通知在动画阶段给角色施加了一个向前的冲量但物理阶段用的是固定步长如果这一帧物理恰好没更新因为累积时间不够一个固定步长冲量就被累积到下一帧和下一帧的重力、摩擦力叠加产生了异常大的速度。修复方案是把冲量改成持续力而不是瞬时冲量或者把动画通知的事件延迟到物理更新前统一处理。这个bug的教训是任何跨系统的状态修改都要考虑两个系统的更新频率是否一致。频率不一致时瞬时修改很容易被放大或丢失。6. 性能与稳定性物理动画系统的调优实战讲完架构和时序最后落到实操。物理和动画系统是性能大户尤其是同屏角色多、场景复杂的时候。这一章分享几个我在项目里验证过的调优手段。6.1 物理调优从休眠到子步进物理性能优化的第一原则是减少参与模拟的物体数量。休眠机制是最有效的——静止超过一定时间的刚体直接标记为睡眠不参与求解直到被外力唤醒。很多项目默认开启但阈值设得太高导致物体明明停了还在算。把休眠阈值调低比如速度低于0.05持续0.5秒就睡能省下大量CPU。第二个手段是降低求解精度。不是所有物体都需要高精度物理。远处的、不重要的物体可以用简化碰撞体甚至用假物理只做位置积分不做碰撞求解。LOD的思想在物理上同样适用。第三个手段是子步进Substepping。当物体速度很快时一帧移动的距离可能超过物体本身尺寸导致隧穿穿过薄墙。子步进把一帧的物理拆成多个小步每步移动距离变小就不会穿过去了。代价是性能成倍增加所以只对高速物体开启。6.2 动画调优骨骼LOD与更新频率动画的性能瓶颈主要在骨骼求值和蒙皮。骨骼LOD的思路是近处的角色用全骨骼远处的角色用简化骨骼比如把手指骨骼合并更远的直接不更新动画用静态姿势或者极低频更新。更新频率也很关键。不是所有角色都需要每帧更新动画。远处的NPC可以每两帧、每三帧更新一次视觉上几乎看不出来但CPU开销直接减半。这个技术叫动画更新节流Animation Update Throttling。还有一个容易被忽视的点是蒙皮方式。CPU蒙皮灵活但慢GPU蒙皮快但有骨骼数量限制。现代引擎大多用GPU蒙皮把骨骼矩阵传到显存在顶点着色器里做变换。如果你的角色骨骼数超过GPU限制就得拆分成多个draw call这时候要权衡。6.3 稳定性抖动、爆炸、穿透的常见成因物理系统不稳定通常有三个原因。第一是质量比过大一个质量1000的物体压在一个质量1的物体上求解器很难收敛表现为抖动。解法是控制场景里物体的质量范围别差太多。第二是迭代次数不足堆叠场景需要更多迭代才能稳定。可以针对特定区域提高迭代次数而不是全局提高。第三是碰撞体形状不合理凹形碰撞体比如L形在物理引擎里处理起来很麻烦容易产生内部碰撞。尽量用凸形碰撞体组合来近似凹形。动画系统的稳定性问题主要是混合权重不归一。多个动画混合时权重之和应该等于1如果因为逻辑bug导致权重和大于1或小于1姿势就会异常。养成习惯混合前先归一化权重。7. 我在实际项目里踩过的几个坑最后分享几个具体的、文档里不会写的经验。第一个坑动画通知里的物理查询用了旧位置。动画阶段角色位置还没被物理更新如果这时候做射线检测判断脚下是不是地面用的是上一帧的位置快速移动时会误判。解法是把这类查询延迟到物理阶段之后。第二个坑根运动和胶囊体不同步。用根运动驱动角色时如果胶囊体的位置更新晚了一帧视觉上角色已经翻过去了碰撞体还在原地导致穿墙。解法是根运动提取后立即同步胶囊体位置不要等物理阶段。第三个坑物理材质和动画事件的冲突。角色踩在不同材质的地面上应该播放不同音效这个判断通常靠物理查询。但如果查询发生在动画通知里而地面物体的物理状态当帧还没更新就会播错音效。这类表现层依赖物理状态的逻辑统一放到物理更新之后处理最稳妥。第四个坑过度依赖物理做角色移动。有些项目想让角色移动完全走物理结果手感很飘因为物理求解有延迟和误差。角色移动这种对响应要求极高的操作最好用运动学Kinematic方式直接控制位置物理只用来做碰撞检测和阻挡不用来做动力学模拟。这些坑的共同点是它们都不是单个系统的问题而是系统交界处的问题。物理和动画各自单独看都很成熟但一旦交接时序、频率、数据一致性就会出问题。理解架构的价值很大程度上就是让你在遇到这类问题时能快速定位到是哪个交界处出了岔子而不是盲目地改参数。如果你正在做角色相关的功能我的建议是先把一帧的更新顺序画出来标清楚每个阶段谁读谁写很多诡异问题会在这张图上自己暴露出来。这比读十篇API文档都管用。