做游戏引擎的人都知道,画面再漂亮,物理系统和动画系统不给力,玩起来一样“纸片人”。在游戏引擎的整体架构里,物理系统和动画系统是最贴近角色手感、最容易让玩家感知到“假”的两个子系统:角色走路时大腿穿进箱子、脚底打滑、被打飞出后空中姿态僵硬,这些都是物理与动画没有配合好的典型症状。这篇是“游戏引擎架构深度解析”系列的第三篇,之前聊过引擎的宏观分层和渲染管线,这次一头扎进物理与动画这两个相对独立但又深度咬合的模块:物理怎么算碰撞、怎么解约束;动画怎么做骨骼、做混合、做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),它利用闵可夫斯基差的概念判断两个凸体是否相交;如果要得到接触点和穿透深度,通常会用 EPA(Expanding 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分级。但不管玩出什么花,底层永远是“模块边界清晰、数据流干净、调试手段完整”这三件事。希望这篇拆解能帮你少走一些弯路。