
做游戏引擎的人都知道画面再漂亮物理系统和动画系统不给力玩起来一样“纸片人”。在游戏引擎的整体架构里物理系统和动画系统是最贴近角色手感、最容易让玩家感知到“假”的两个子系统角色走路时大腿穿进箱子、脚底打滑、被打飞出后空中姿态僵硬这些都是物理与动画没有配合好的典型症状。这篇是“游戏引擎架构深度解析”系列的第三篇之前聊过引擎的宏观分层和渲染管线这次一头扎进物理与动画这两个相对独立但又深度咬合的模块物理怎么算碰撞、怎么解约束动画怎么做骨骼、做混合、做IK两者在架构上如何分工、如何通信以及实际项目中踩过的性能坑和兼容性坑。文章适合三类人看正在自研引擎或游戏底层、需要和物理或动画死磕的客户端程序员想搞懂手感为什么“肉”的技术美术以及想给角色加“物理生动感”的独立开发者。尽量用大白话把复杂的架构讲清楚给可以直接上手的调参建议。1. 物理与动画为什么在引擎里总被放在一起先搞清楚定位和边界1.1 物理系统到底在算什么物理系统在游戏引擎里干的事远比“物件掉落”要多得多。它的核心职责是对场景中的刚体、软体、车辆、布娃娃ragdoll、布料等做碰撞检测和动力学仿真得出每个对象在下一帧的位置、朝向、速度与角速度。严格来说这属于“预测性仿真”引擎只负责根据当前状态和外部施加的力推算出下一时刻的状态。物理系统要和渲染系统解耦这是一个很关键的架构决策。渲染管线关心的是每秒钟显卡能输出多少帧物理系统关心的是数值稳定性和确定的模拟结果。渲染帧率可以随硬件波动60帧、120帧甚至144帧都能跑但物理模拟一般不能跟着渲染帧走——如果物理步长跟着渲染帧变低帧率下物体会出现隧道穿透高帧率下行为又会变得不稳定。所以主流引擎都采用“固定时间步长”做物理无论渲染多少帧物理每帧固定推进比如33.3毫秒或16.6毫秒用累计时间差的方式追上真实时间。这也是为什么许多人把物理系统称作引擎里的“世界计算器”。1.2 动画系统在干什么和物理系统什么关系动画系统负责计算角色的骨骼姿势。从资源层看动画系统要做的事包括对导入的动画剪辑做采样通常是骨骼旋转、位移曲线连接动画状态机做状态切换与过渡管理多轨动画的混合混合树以及在必要的时候进行逆向运动学IK解算。最终输出是一个“骨骼姿势”再把骨骼姿势生成蒙皮矩阵交给渲染系统去驱动角色网格变形。动画系统早期和物理系统是互不相干的两条线。动画只管按曲线演物理只管算世界里的刚体。但到了现代游戏引擎这两者之间已经不可能完全隔离了角色受击时要播放受击动画受击后身体可能被击飞击飞后落到障碍物上又应该有一个碰撞反馈角色走路时脚步必须贴合地形脚不对地就要做IK修正布娃娃死亡动画更是直接把骨骼交给物理系统驱动。所以说物理与动画在架构上是两个独立模块但在数据和运行时上是深度耦合的它们之间的接口设计是否清晰几乎直接决定了手感的上限。1.3 模块边界怎么划才不会失控翻看Unity、Unreal、自研引擎的模块图物理和动画一般不会像渲染那样横跨整条GPU管线而会被归到“模拟层”Simulation Layer。因为它们处理的都是“让世界动起来”的事都是CPU密集型工作都需要面向实时性能做特殊的数据结构设计。两者的调度周期、优先级、是否可并行往往也由同一个“模拟调度器”管理。这也引出一个架构原则物理与动画应该都作为后台子系统通过明确的接口向游戏逻辑提供能力而不是把物理代码、动画代码散落在每个游戏对象里。我在项目里见过最痛苦的情况就是每个游戏玩法脚本里都直接调用物理API和动画API到最后物理步长一改、动画钩子一换整个项目都崩。把功能收敛到子系统隔离是在架构层面必须做的一件事。2. 物理系统拆开看碰撞检测、刚体求解、约束与调度2.1 碰撞检测管线Broadphase、Narrowphase 与 CCD碰撞检测是物理引擎里最贵也最容易写崩的部分。大体上现代物理引擎把碰撞检测分成两个阶段Broadphase 和 Narrowphase。Broadphase 负责“快速排除绝对不可能碰到的对象”它的产出是一组“潜在的碰撞对”。常见做法是扫描剪裁法Sweep and Prune或包围盒层级树BVH。SAP 的思路是把所有物体按世界坐标轴的方向排一个有序链表每帧沿着一个轴扫描如果两个物体在这个轴上的投影区间重叠就暂定它们可能碰撞。区间重叠就放到候选列表里交给 Narrowphase 做精确检测。BVH 则是把物体递归装进包围盒通过树的剪枝快速排除掉不相交的子树。Narrowphase 接收候选对做精确的几何求交。凸体最常用的是 GJK 算法Gilbert–Johnson–Keerthi它利用闵可夫斯基差的概念判断两个凸体是否相交如果要得到接触点和穿透深度通常会用 EPAExpanding Polytope Algorithm在 GJK 之后做扩展。这两个算法的组合是引擎开发者的标准配置。对于非凸网格比如地形、静态建筑一般不会直接用 GJK而是把网格拆成凸包或者用三角网格碰撞体配合专门的三角形碰撞检测。隧道效应是高复杂度物体高速移动时的经典问题一颗子弹一帧移动了2米而一颗子弹碰撞体的厚度只有0.05米这帧开始子弹在墙前、这帧结束子弹在墙后计算完就穿过去了。解决隧道效应的标准手段是连续碰撞检测CCD原理是把物体扫过的路径当做一个“扫掠体”swept shape用扫掠体做求交。注意 CCD 也不是全免费——它对性能损耗比较大实践里通常只在“必须防穿透”的近战武器、子弹、绳索等对象上开启。2.2 刚体动力学与迭代求解器为什么物理要固定步长有了接触点还不够物理引擎必须算出每个刚体下一帧的速度和角速度。刚体动力学的基础是牛顿第二定律力 质量 × 加速度。引擎把物体在时间步长内收到的所有外力、力矩汇总用半隐式欧拉法积分更新速度与位置。为什么用半隐式欧拉而不是更“准确”的RK4因为游戏引擎要的是稳定性和实时速度半隐式欧拉简单、稳定、只要能满足约束精度就可以接受除非是布料模拟这种需要更精细积分的场景。真正的复杂度集中在约束求解器上。刚体彼此接触、关节约束关节这些都可以抽象成一系列约束方程求解器在每个时间步内多次迭代逐步修正刚体的速度和位置使约束尽量被满足。广泛使用的算法是顺序冲量法Sequential Impulse也叫PGS它的思路是每帧重复若干轮每轮按顺序处理每个接触点的冲量把不满足约束的“速度差”修正回来。迭代次数一般取4到10次次数越多解越收敛但性能越差。物理引擎里常听到的“堆箱子不稳”“关节太软”多半是因为迭代次数不足或者接触点筛选太粗糙。固定时间步长是求解器稳定的关键。如果步长变大数值积分会变得不稳定刚体容易飞出去俗称“爆炸”步长变小世界里的物理会变慢套数据。所以引擎宁可做“物理累加器”也不让物理步长跟着渲染帧走。这也是为什么改渲染帧率不会导致物理行为改变的底层原因。2.3 约束、关节与物理材质不是魔法是算出来的关节是物理引擎里的“胶水”铰链关节、固定关节、滑块关节、弹簧关节……每个关节背后其实都是一组约束方程。比如一个“铰链”约束会限制两个刚体在公共旋转轴上保持对齐同时放开沿着旋转轴的转动自由度。求解器要做的就是在迭代过程中不断根据这些约束产生修正冲量。物理材质决定表面摩擦与弹性。静摩擦力要避免物体滑动动摩擦力需要耗散能量回弹系数控制碰撞后的反弹程度。有三个参数我最常调摩擦系数0和1之间通常有巨大体验差异、回弹严格说叫 bounciness 或者 restitution、以及阻尼物体的线性与角速度衰减。注意物理材质不是越贵越好——摩擦系数过大角色贴墙走会像“吸住”一样回弹过大会出现“跳气球”的坏手感。2.4 调度与多线程物理不能抢渲染的CPU现代物理引擎都有自己的作业调度器能把 broadphase、narrowphase、求解器、约束更新等步骤拆成任务在多个工作线程上并行跑。Unity 的 DOTS Physics 和 Unreal 的 Chaos 本质上都走Job Multithread 的路子。但物理并行化的边界要人为划清楚通常“物理仿真”在后台线程里跑渲染主线程只接收物理结果物理事件碰撞回调要排队发到主线程不然线程安全问题会让项目莫名崩溃。还有两个实践原则静态碰撞体和动态碰撞体一定分开管理。静态对象不参与动力学求解只参与碰撞检测能节省大量计算。物理步进产生的“每步之间”的渲染插值非常重要物理是离散的渲染是连续的不插值的话低物理帧率下物体会“一卡一卡”。很多引擎在渲染阶段用上一步和下一步的位置做线性插值这个细节直接影响观感。3. 动画系统拆开看骨骼、状态机、IK 与根运动的工程实践3.1 骨骼动画与蒙皮数据从哪来、到哪去骨骼动画的数据模型是一个树形层级根节点、骨盆、脊柱、左右臂……每个节点叫一个“骨骼”或“关节”骨骼之间是父子关系。动画剪辑存储的就是每根骨骼在每一帧的局部旋转欧拉角或四元数和局部位移。采样时引擎按时间索引读取旋转和位移做插值再把局部变换乘到父骨骼的全局变换上最终得到每根骨骼的全局矩阵。这个全局矩阵转换成“蒙皮矩阵”后由渲染顶点着色器使用。GPU蒙皮就是把每个顶点的参考骨骼索引和权重与蒙皮矩阵相乘得到最终顶点位置。做渲染时要注意顶点上限、骨骼索引数量、权重数量都会影响GPU带宽。实践中一个角色如果超过64根骨骼且要开GPU蒙皮要注意骨骼索引打包成几个字节在移动端是有讲究的。动画曲线数据本身通常不直接存成矩阵而是存四元数与向量再用采样工具抽稀。3.2 动画状态机与混合用户看的是表现项目调的是参数动画状态机是根据输入状态移动方向、速度、跳跃、受击切换动画剪辑的逻辑层。状态机本身不难难的是过渡和混合。从Walk切到Run如果硬切角色会“瞬移”一段正确做法是“交叉淡入淡出”crossfade在过渡时间内按权重把两段动画混合起来。过渡时长一般在0.1~0.3秒之间起身、攻击打断、死亡这种硬切换可以缩短到0。混合树是对多个动画片段做基于参数的程序化混合比如以水平速度分量为X轴、垂直速度为Y轴对不同脚步循环动画做二维混合这样任何速度和方向下角色都能走出“自然步”。动画事件是规划伤害判定的关键。动画播到第几帧该触发脚步或者伤害判定不应该硬编码在逻辑代码里。正确做法是在动画剪辑里面打 Event用时间点触发不要用帧号——因为帧率可变帧号在不同设备上会错位。事件回调要统一由动画管理层分发到游戏逻辑层不能让逻辑层直接扫骨骼上的曲线。3.3 逆向运动学IK救命的“脚对地”技术IK要解决的问题是程序控制的骨骼末端手、脚需要尽量贴合目标位置地面、手柄、视线目标。比如角色上台阶目标点离地面高了脚必须抬起来但动画数据没有这个抬脚动作这时就需要IK修正。IK实现方式主要有三种分析式IK通过几何关系直接解出关节旋转适合固定长度骨骼速度快但通用性差。CCD循环坐标下降从末端开始向头逐关节旋转调整简单、不死锁适合绝大多数骨骼链。FABRIK用迭代计算目标位置到根节点之间每个节点位置收敛快、视觉效果好适合处理多肢。IK在架构上属于动画后处理阶段状态机把基础姿势算出来后IK再对脚踝、腕部等末端做修正。做IK时最关键的细节是“别让IK和物理对抗”比如角色踩在移动的平台车上IK修正把脚固定在目标点上平台车一开走角色腿被扯成麻花。正确做法是给IK加平滑与失效判定目标点本身也应该由物理射线检测的结果稳定输出来。3.4 根运动Root Motion是把双刃剑根运动是指角色整体的位移、朝向、升降隐藏在这条“根骨骼”的动画曲线里播放动画时引擎用这些曲线推动角色移动。典型场景翻滚、受击后撤、处决动画。用根运动的好处是动画师可以对特殊动作完全控制位移节奏不依赖程序员写的移动代码坏处是容易和路径导航、角色控制器打架做完一个动作后角色的坐标可能和预期位置差一大截。架构上的建议是把根运动作为动画系统的“可开关载荷”默认关只在需要精确编排位移的动画上开打开后游戏逻辑不算位移但要把每帧的根运动位移解析出来做碰撞检测和脚踏实地修正。不这么做就会遇到“角色翻滚进墙里”这种经典翻车现场。4. 物理与动画协同实战布娃娃、布料、调参与问题速查4.1 布娃娃、布料与混合驱动布娃娃是物理和动画最经典的结合场景角色死亡或受击后从“动画驱动骨骼”切到“物理驱动骨骼”。实现时要注意“过渡”而非“硬切”角色在空中被击飞时先播放几帧受击动画再启用布娃娃否则观感会瞬间“塌掉”。骨骼体要轻布娃娃的每个骨骼都对应一个胶囊体/球体碰撞体碰撞体要和角色视觉尺寸严格匹配。调不好会出现“尸体悬空”或“尸体陷进地板”的诡异效果。布娃娃启用后要设置“生命周期”多少秒后让尸体“死亡”彻底停止模拟并标记不再与交互对象碰撞不然每具尸体都持续模拟CPU会顶不住。布料头发、披风、裙摆是用弹簧约束或轻量物理模拟来表现的不应当走完整刚体不然一来性能扛不住二来极易抖动。如果你的引擎把头发做成刚性布娃娃系统你一定会后悔。现代游戏里头发的物理一般是“束带约束”“速度阻尼”的专门方案或者用可拉伸的粒子弹簧。4.2 调优物理与动画的几个关键参数物理系统的调参优先级按“影响手感排序”固定步长60Hz感觉远比30Hz舒服30Hz会出现微卡和黏连感。迭代次数4与10的差距在滑动摩擦场景能明显感知但8以上收益递减。接触偏移量contact offset默认值太大角色会“浮”在墙上太小又会抖动。摩擦与回弹和上述两项联合调才更像现实。动画系统的调优点主要是LOD多人游戏里远处角色用1/2精度动画降低采样率或减少骨骼数近处全精度。动画压缩对四元数曲线做量化对高精度曲线抽稀一个蹲起动作可能只需要8~12个关键采样点。骨骼合并与剔除对不开物理的静态配角直接烘焙一个动画到顶点缓存减少CPU蒙皮开销。4.3 常见问题速查表物理与动画合体篇这是我从实际项目中整理出来的高频问题排查清单可以直接抄作业现象大概率原因排查与解决思路角色穿过薄墙或子弹穿透没有开CCD、速度过高、碰撞体过薄对高速物体开启CCD把碰撞体做成胶囊或方块而非片状堆箱子一碰就倒迭代次数偏低、接触点筛选粗糙把迭代次数提到8检查动态刚体是否意外进入休眠状态角色被挤出墙角或卡墙角碰撞体尺寸和动画骨骼不对齐检查胶囊体半径是否大于动画腿部曲率调整contact offset布娃娃抽搐骨骼碰撞体重叠、求解过度缩小碰撞体间隙检查是否有物体同时被多个关节约束动画与物理明显不同步脚半插地板动画后处理IK在物理后才做且没有用物理结果做修正IK目标和物理碰撞结果绑定做射线检测时考虑前一个物理步位置角色播放受击动画时仍能被推飞动画和物理没有通信受击时把角色动画冻结同时激活物理角色的击飞冲量接口4.4 多线程与网络同步物理与动画是多人游戏的两大麻烦多人游戏中物理和动画都要做网络同步。物理同步有三个惯用方案快照同步每隔N毫秒同步位置与角速度、状态同步加插值、确定性重放。动画同步也有惯用方案同步的是“片段名参数时间”而不是每根骨骼矩阵。如果直接同步骨骼矩阵带宽会被瞬间打满。再提一个非常容易忽略的坑物理和动画的浮点确定性。不同CPU架构x86 vs ARM、不同编译器优化级别下同样运算的结果可能有微小差异单机没感觉帧同步对战里就崩了。帧同步游戏如果用了物理就要保证物理模块是确定性的通常做法是全程整数化或者是严格定点运算或者干脆物理全逻辑端把输出结果回传显示端进行插值。5. 调试物理与动画必须用对工具DebugDraw、曲线可视化与帧步进5.1 DebugDraw 是最重要的物理调试工具没有DebugDraw你几乎不可能调好刚体堆叠、关节松紧和碰撞体尺寸。DebugDraw 要能显示三类信息碰撞体轮廓每个碰撞体的形状、旋转、缩放必须在场景视图里一眼能看到。碰撞体比视觉网格大一圈还好小一圈就会穿模这种问题只能靠画出来才发现。接触点与法线接触点的位置和法线方向要实时画出来。堆箱子不稳的时候看一眼接触点分布就知道是哪几个点算错了。冲量与速度箭头把每帧的冲量或速度以箭头显示能快速定位“为什么物体被推飞了”这类问题。我做项目时有个习惯物理调模式必须能在运行时一键切换“画碰撞体/不画碰撞体”并且能暂停物理步进来逐帧看接触点。没有这个能力你会在“玄学调参”里浪费大量时间。5.2 动画曲线可视化与骨骼调试动画调试要解决的是“这个动作在参数空间里到底长什么样”。至少要有两个视图骨骼坐标系视图把每根骨骼的局部坐标系画成三色箭头X/R、Y/G、Z/B在角色模型上叠加。看IK和root motion的时候箭头的方向能直接告诉你旋转偏移出在哪。动画曲线视图选一个骨骼把它的旋转曲线、位移曲线按时间画出来并标志出当前采样时刻。凡是“某个动作播放到一半卡住又跳回来”的问题90%是曲线硬切或者插值类型设置错了。另外强烈建议做“帧步进”调试把动画系统暂停在当前帧逐帧推进检查状态机转换的那一帧到底发生了什么。这比在游戏里反复触发同一个动作要高效得多。5.3 性能剖析物理与动画的 Profile 数据怎么看物理剖面要关注四个指标物理步耗时、Broadphase产生的候选对数、Narrowphase检测对数、求解器求解时间。如果候选对数突然翻十倍多半是有人在一帧内加了大量动态碰撞体比如把绳子的每节都做成了刚体。狭窄场景里候选对数高是正常的但物理耗时高就不正常优先排查“谁在每帧创建和销毁碰撞体”。动画剖面要关注三个指标动画采样耗时骨骼数×动画片段数、蒙皮耗时、IK耗时。蒙皮耗时和顶点数强相关动画采样耗时和同时播放的动画轨数量强相关。遇到动画瓶颈先查“是不是所有角色都在全精度采样”再查“有没有角色根本没有进入视野还在更新骨骼”这两条排查完能解决大部分性能问题。5.4 抓帧与回放物理和动画问题的“案发现场”物理和动画的bug往往是时序性的很难靠肉眼凑巧撞见。最有效的做法是做“状态回放”把每帧的物理世界状态所有刚体的位置、速度、激活状态和动画状态机的状态记录到环形缓冲里出问题后一键保存成回放文件再用调试器逐帧播放。这个能力在引擎层面不太难实现但价值极高——布娃娃抽搐、穿透、动画跳变这类偶发问题靠回放定位的效率至少翻三倍。我自己在接一个大型项目时就吃过这个亏布娃娃偶尔会在玩家靠近时“弹飞”排查了一周没抓到现场最后加了状态回放五分钟就定位到是某个触发器的碰撞体在激活时对布娃娃施加了一次极大冲量。这个经验让我养成了“先做记录再做预判”的调试习惯。6. 关于物理与动画我最后想说的几句实在话做了这么多年引擎最深的感受是物理系统和动画系统强的项目不一定渲染多惊艳但玩起来就是“顺”。很多团队把大量精力砸在画质上却忽视了这两个直接影响手感的后端模块结果游戏截图好看一上手就露馅。根据个人经验建议每个项目从第一天起就把三件事纳入管线物理DebugDraw常开、动画骨骼可视化常备、帧步进调试器可用。谁用谁知道这比任何花哨的AI调参工具都实在。还有一点物理步长和动画事件这类基础参数在项目启动阶段就定好中途改一次的成本高到你想哭——我就因为从30Hz物理升级到60Hz重调了整整一个月的角色手感。这个内容后续还可以扩展的方向很多比如物理驱动的程序化动画、基于机器学习的动作过渡、大规模场景下的物理LOD与动画LOD分级。但不管玩出什么花底层永远是“模块边界清晰、数据流干净、调试手段完整”这三件事。希望这篇拆解能帮你少走一些弯路。