物理引擎与游戏的集成
Integrating a Physics Engine into a Game
碰撞/物理世界里的刚体只是抽象的数学描述,要通过游戏对象这个中枢,间接连到屏幕上的视觉表现。刚体按谁驱动分三类:物理驱动、游戏驱动(Havok 称 key framed)、固定;Havok 用可在运行时切换的"运动类型"统一表示。每帧更新有固定顺序:先更新游戏驱动刚体、幻影、力、冲量和约束,再步进模拟,然后回读物理驱动对象、查询幻影、执行投射查询。物理宜用恒定的 1/30 或 1/60 秒时间步。GEA 还逐一讨论了子弹、手雷、爆炸、可破坏物、角色移动、相机碰撞和布娃娃的实际做法(GEA4 卷 II 14.5–14.6)。
游戏对象与刚体的连接
通常不直接画刚体(调试除外),刚体用来描述游戏世界中逻辑对象的形状、大小和物理行为。物理世界与渲染之间以游戏对象为中枢(图 14.35),另有一条可选的调试直绘路径(卷 II 14.5.1)。一个游戏对象对应零个或多个刚体:
- 零个:没有碰撞表示,表现为非实体。例如头顶飞过的鸟、看得到但到不了的远景,或碰撞由游戏自行手工处理的对象。
- 一个:多数简单对象。碰撞体形状贴近视觉形状,刚体的位置和朝向与游戏对象完全一致。
- 多个:角色、机械、载具等由多个可相对运动的实体部件组成的对象。通常用骨骼跟踪各部件,刚体与关节一一对应其位置和朝向:动画驱动关节时刚体跟着走,物理驱动刚体时再间接控制关节。映射不一定一对一,有些关节完全由动画控制。
通常由每个游戏对象管理自己的刚体:按需创建和销毁、加入和移出物理世界、维护刚体与对象或关节之间的位置同步。多刚体对象可以用一个包装类管理,让游戏对象不必处理细节,也让各类对象管理刚体的方式保持一致。
三类刚体
- 物理驱动 (Physics-Driven):运动完全由模拟决定,例如碎片、爆炸的建筑、滚下山坡的石头、空弹匣、弹壳。每步模拟后向物理系统查询刚体变换,再应用到整个游戏对象或其中某个关节(卷 II 14.5.1.1)。
- 例:门可炸飞的保险箱。一个三角网格分箱体和门两个子网格,配一个两关节骨骼(根关节绑箱体,子关节绑门)。碰撞几何也分两块,生成两个独立刚体,分别连到两个关节,再加一个铰链约束让门相对箱体正确摆动。刚体运动更新关节,动画系统生成矩阵调色板后,渲染就画在刚体的位置。炸门时断开约束、施加冲量。玩家看来门和箱体成了两个对象,实际仍是一个游戏对象、一个网格、两个关节、两个刚体。
- 游戏驱动 (Game-Driven):按非物理方式运动(动画、样条路径、玩家控制),要参与碰撞(例如把物理物体推开),但不希望物理系统干扰它的运动(卷 II 14.5.1.2)。
- 不受重力影响,被视为无穷大质量(通常记为质量 0,因为这对物理驱动刚体是无效值),所以力和碰撞冲量都改变不了它的速度。
- 不能每帧直接设置位置:这会造成不连续,物理驱动刚体可能突然与它穿透,却拿不到它的动量来解决碰撞。应当用冲量移动它,使其在步末恰好到达目标位置。多数 SDK 提供便利函数,计算到达下一帧目标位置和朝向所需的线冲量与角冲量。要停下时必须把速度清零,否则它会沿最后的非零轨迹一直走下去。
- 例:动画开关的保险箱门。角色走近、拨号、开门、放钱、关门锁门的整段动画中,刚体处于游戏驱动模式,动画驱动关节,关节驱动刚体。之后另一个角色炸门时,切换到物理驱动模式、断开铰链、施加冲量。拨盘需要一个额外的子网格和关节,但不需要刚体,除非希望它爆炸时也飞出去。GEA 指出这个例子其实不需要铰链约束;只有门要停在半开状态,并在箱体被挪动或门被碰到时自然摆动,才需要它。
- 固定 (Fixed):建模游戏世界的静态部分。类似游戏驱动刚体,但完全不参与动力学模拟,只参与碰撞。大型静态世界里只有少量动态物体时,性能收益很大(卷 II 14.5.1.3)。
- Havok 的运动类型 (Motion Type):所有刚体都是
hkpRigidBody,用一个字段区分固定、key framed(游戏驱动)和 dynamic(物理驱动)。固定类型创建后不可更改,其余可以在运行时切换。例如角色手里的物体是游戏驱动,一扔出去就切成物理驱动。运动类型还兼作惯性张量提示("球形惯性的 dynamic""盒形惯性的 dynamic"等),Havok 据此做优化(卷 II 14.5.1.4)。
每帧更新物理
完整的物理更新不只是步进模拟,还要维护游戏对象与刚体的连接、施加游戏的力和冲量。通常按以下顺序(卷 II 14.5.2):
- 更新游戏驱动刚体:让它们的变换匹配对应的游戏对象或关节。
- 更新幻影:幻影相当于没有刚体的游戏驱动碰撞体,用于某些碰撞查询,需在步进前就位(见 碰撞查询)。
- 更新力、施加冲量、调整约束:例如检查可断铰链是否已断,断了就让物理引擎移除该约束。
- 步进模拟:积分、碰撞检测增删接触、解决碰撞、满足约束(见 碰撞响应与约束求解)。视 SDK 而定,可能封装成一个原子的
step(),也可能分阶段单独调用。 - 更新物理驱动游戏对象:取出刚体变换,写回游戏对象或关节。
- 查询幻影:读取步进后的接触,用来做决策。
- 执行投射查询:同步或异步地发起射线和形状投射,结果出来后供各系统决策。
射线和形状投射理论上可以在游戏循环中任意时刻进行。游戏驱动刚体和力要在步进前处理,模拟才"看得见";物理驱动对象要在步进后更新,才能用上最新变换。渲染通常放在最后,保证画出的是同一时刻一致的世界。碰撞查询该放在帧内哪个时机,见 碰撞查询。
单线程循环示例与固定时间步
GEA 的单线程主循环(卷 II 14.5.2.2):
F32 dt = 1.0f/30.0f;
for (;;) // main game loop
{
g_hidManager->poll();
g_gameObjectManager->preAnimationUpdate(dt);
g_animationEngine->updateAnimations(dt);
g_gameObjectManager->postAnimationUpdate(dt);
g_physicsWorld->step(dt);
g_animationEngine->updateRagDolls(dt);
g_gameObjectManager->postPhysicsUpdate(dt);
g_animationEngine->finalize();
g_effectManager->update(dt);
g_audioEngine->update(dt);
// etc.
g_renderManager->render();
dt = calcDeltaTime();
}
- 游戏对象分三阶段更新:动画前(比如排队新动画)、动画算出最终局部姿势和暂定全局姿势之后(最终全局姿势和矩阵调色板生成之前)、物理步进之后。动画管线各阶段见 动画管线与动作状态机。
- 游戏驱动刚体的位置一般在
preAnimationUpdate()或postAnimationUpdate()中设置;物理驱动刚体的位置一般在postPhysicsUpdate()中读取。 - 时间步:多数积分器、碰撞算法和约束求解器在 恒定时表现最好(见 数值积分)。建议用理想的 1/30 或 1/60 秒步进物理 SDK,再控制整个循环的帧率。掉帧时宁可让物理在视觉上变慢,也不要跟着实际帧率调整模拟步长(注意书中示例循环本身把可变
dt传给了step(),以及另一种"固定步长 + 累加器"做法,见文末)。与游戏循环模式的关系见 游戏循环与更新方法。
实际用例
- 简单刚体对象(卷 II 14.5.3.1):武器、可捡起投掷的石头、空弹匣、家具、架上可射落的物品。可以写一个持有刚体引用(如
hkpRigidBody)的自定义游戏对象类,也可以写成可挂到任何对象上的附加组件。这类对象常在运行时切换运动类型:拿在手里时游戏驱动,掉落后物理驱动。 - 子弹(卷 II 14.5.3.2):
- 射线投射实现:开火那一帧发一条射线,立即作用于命中物。但它不考虑飞行时间和重力下坠。需要这些时用真正的刚体弹道,尤其适合慢速投射物(投掷物、火箭),例如《最后生还者》中扔出的砖块。
- 射线起点问题:从相机焦点发出还是从枪口发出?第三人称射击中,枪口射线和"相机焦点 → 屏幕中心准星"的射线通常不重合,可能准星压在目标上,角色却明显被障碍物挡住。通常要用各种技巧,让玩家觉得打中了瞄准的东西,同时画面仍然合理。
- 碰撞几何与可见几何不匹配:玩家从小缝里看得到目标,碰撞几何却是实心的(一般只对玩家角色是问题)。一种解法是用渲染查询替代碰撞查询:在某个渲染 pass 中生成每像素存游戏对象唯一 ID 的纹理,查准星下的像素属于谁。
- 动态环境中,投射物有飞行时间时 AI 需要打提前量。
- 命中效果:Unreal 用物理材质,可见几何除了视觉材质还可以标记物理材质,定义撞击声、弹着点粒子(squib)、贴花等。Naughty Dog 的做法类似:碰撞几何标记多边形属性(PAT),定义脚步声等;但子弹命中要直接与可见几何交互,所以可见材质可以附加可选的子弹效果,按投射物类型指定 squib、撞击声和贴花。
- 手雷(卷 II 14.5.3.3):做成自由物理对象会大幅失去控制。可以施加人为的力或冲量找回一些控制,例如第一次弹起后施加极强的空气阻力,限制它弹离目标的距离。有的团队完全手工管理:事先用一串射线投射算出弧线,甚至显示给玩家;扔出后沿弧线移动,并精心控制反弹,使它不会离目标太远又看起来自然。
- 爆炸(卷 II 14.5.3.4):由视觉效果(火球、烟)、音效(爆炸声及冲击物体的声音)和逐渐扩大的伤害半径组成。半径内的对象通常扣血,并用动画(角色的反应往往这样做最好)或动力学模拟表现冲击波。后者对半径内物体施加径向冲量:方向是从爆心到物体中心的归一化向量,乘以爆炸强度,可随距离衰减。还可以给植被动画系统施加"力",让草木短暂弯折。
- 可破坏物(卷 II 14.5.3.5):初始时必须看起来是一个整体,又要能碎成很多块,可能逐块剥落,也可能一次性炸碎。
- 实现:DMM 这类可变形体模拟能自然处理破坏;用刚体实现则把模型分成若干可破坏块,每块一个刚体(Havok Destruction 就这样做)。可以先用单块"完好"的视觉和碰撞几何,需要破碎时换成损坏版本;砖堆、锅碗堆这类则一直按多块建模。
- 简单堆叠刚体在好的物理引擎里可行(但不总是容易调对),但做不出"好莱坞式"效果:结构定义(墙基、车架不可破坏;非结构块被打就掉;结构块掉落时还会对上面的块施力;爆炸块引发二次爆炸或传播伤害)、与掩体系统的关联、生命值(累积伤害后整体坍塌,或每块要挨几下才碎)、用约束让碎块挂在本体上、逐渐坍塌(长桥一端被炸,坍塌应慢慢传到另一端才显得沉重)。物理系统不会白送这些:它只会同时唤醒整个模拟岛。这类效果靠审慎使用游戏驱动运动类型实现。
- 角色机制(卷 II 14.5.3.6):保龄球、弹球、《Marble Madness》这类以球为主角的游戏,可以直接用自由刚体加力和冲量控制。人形或动物角色的运动太复杂,力和冲量控制不好,所以通常:
- 用一组游戏驱动胶囊刚体挂在动画骨骼的关节上,主要用于子弹命中检测和次级效果(手臂把桌上的东西碰掉)。它们是游戏驱动的,不会避开不可移动物体,动作是否可信由动画师负责。
- 移动时,多数游戏朝目标方向做球或胶囊投射探测,手工解决碰撞。这样可以做到:斜撞墙时沿墙滑动;跨上低矮路沿而不被卡住;走下低路沿时不进入"下落"状态;坡太陡就滑回去(多数游戏有截止角);根据碰撞调整动画。例如垂直撞墙时,可以让角色永远对墙"太空步",可以放慢动画,也可以播一段伸手扶墙的动画,然后合理地待机,直到移动方向改变。
- Havok 的角色控制器把角色建模为胶囊幻影,每帧移动以寻找潜在新位置,并维护一个去噪的碰撞接触流形(一组接触平面),游戏据此决定如何移动角色、调整动画(图 14.36)。
- 相机碰撞(卷 II 14.5.3.7):相机绝不能穿入场景几何,否则破坏真实感。基本做法是用一个或多个球形幻影或球投射包围虚拟相机,快碰到东西时提前调整位置或朝向。看似简单,实际极其棘手,需要大量试错;很多团队整个项目期间都有一名专职相机工程师。要点:
- 拉近相机在很多情况下效果好,第三人称甚至可以一直拉到第一人称(注意别穿进角色的头)。
- 大幅改变水平角通常不好,会打乱相对相机的操控;瞄准时尤其会激怒玩家。只是跑动时调整则很自然,所以可以只在非战斗时允许水平调整。
- 竖直角可以适度调整,过度会让玩家看不到地平线,最后只能俯视角色头顶。
- 有的游戏让相机沿竖直平面内的一条弧线(可用样条描述)移动,一个输入(如左摇杆竖直偏移)同时控制缩放和竖直角。Naughty Dog 引擎的相机就是这样。碰撞时可以沿弧线自动移开、水平压缩弧线等。
- 还要考虑相机前方:柱子或其他角色挡在相机和主角之间时,有的游戏让遮挡物半透明,有的拉近或绕开。处理方式可以成就也可以毁掉玩家对游戏品质的感受。务必预留大量试错时间。UE5 一侧见 相机系统。
- 布娃娃集成(卷 II 14.5.3.8):
- 有意识的角色:整体移动靠形状投射或移动幻影,身体细节由动画驱动,四肢可挂游戏驱动刚体用于武器瞄准或撞倒物体。失去意识后布娃娃接管:四肢是由约束连接的胶囊刚体,连到骨骼关节,由物理模拟驱动骨骼。
- 两套碰撞表示:活着时刚体是游戏驱动的,穿透无所谓,而且通常希望它们重叠,免得留下敌人能打穿的缝;变成布娃娃后刚体绝不能互相穿透,否则碰撞解决会施加巨大冲量,四肢向外"爆开"。所以角色常在有意识和无意识时使用完全不同的碰撞/物理表示。
- 过渡:在动画姿势和物理姿势之间简单 lerp 往往不好,因为物理姿势很快偏离动画姿势,两个无关姿势的混合不自然。过渡期可以用动力约束(见 碰撞响应与约束求解)。
- 初始穿透:有意识的角色常与背景几何穿透,切换成布娃娃时刚体可能已在实体内部,产生巨大冲量和狂乱的布娃娃表现。对策:精心制作死亡动画,让四肢尽量不碰东西;在游戏驱动阶段用幻影或碰撞回调检测接触,身体任何部位一碰到实体就立即切换成布娃娃。
- 单面碰撞非常重要:嵌进墙里一半的肢体会被推出来,而不是卡在里面。但它也不能解决所有问题:角色移动太快或过渡没做好时,某个刚体可能跑到薄墙另一侧,导致角色悬在半空,倒不到地上。
- 起身:要找第 0 帧姿势最接近布娃娃静止姿势的"起身"动画(静止姿势通常不可预测),可以只匹配大腿根、上臂等少数关键关节;或用动力约束在静止前把布娃娃引导到适合起身的姿势。
- 约束设置很棘手:四肢要能自由运动,又不能做出生物力学上不可能的动作,这也是布娃娃常用专用约束类型的原因之一。Havok 这类高质量引擎提供内容工具,美术可以在 Maya 等 DCC 中设置约束并实时测试。GEA 总结:让布娃娃工作不难,让它好看要花大量功夫,务必预留试错时间。与动画系统的衔接见 动画约束与后处理。
高级物理特性
带约束的刚体模拟覆盖面已经很广,研究正把物理引擎扩展到刚体之外(卷 II 14.6):
- 可变形体:DMM 是很好的例子。
- 布料:由硬弹簧相连的质点片建模,以难调著称(与其他物体的碰撞、数值稳定性等)。不过许多游戏和 Havok 等第三方 SDK 已经提供强大且表现良好的布料模拟。UE5 一侧见 Chaos 布料。
- 毛发:用大量物理模拟的细丝,或更简单地用贴了头发纹理的布片,再调整布料模拟让头发动得可信。《神秘海域:失落的遗产》中 Chloe 的头发就是这样做的。
- 水面与浮力:浮力可以用专门系统实现,也可以用物理模拟中的力实现。水面的有机运动往往只是渲染效果,不影响物理;物理上水面常被当作平面,大幅位移时移动整个平面。也有团队在推进动态水面、卷浪和真实水流,例如上帝模拟游戏《From Dust》。
- 通用流体:目前主要属于专门的模拟库。《LittleBigPlanet》系列用 2D 流体做烟火效果;PhysX SDK 提供基于位置的 3D 流体模拟。
- 基于物理的音频合成:碰撞、弹跳、滚动、滑动需要相应的声音。可以受控地回放预录音频片段,动态合成正在成为可行的替代方案。
- GPGPU:用 GPU 做碰撞和物理,例如 Naughty Dog 的布料模拟引擎完全运行在 GPU 上。
与 UE5 的对照
- UE5 的碰撞与物理规则见 Chaos 物理与碰撞。UE5 文档说明物理资产同时服务于布娃娃和物理动画(刚体随动画模拟)(Physics Asset Editor / Physics Bodies、Physics in Unreal Engine),相当于 GEA 布娃娃集成中"动画 ↔ 物理"的切换与动力约束。
- 角色移动方面,GEA 描述的"胶囊投射探测、手工解决碰撞"与 UE5 角色移动组件的思路可以对照阅读,见 角色移动组件与 Mover(这一对照是归纳,GEA 原文只讨论 Havok 角色控制器)。
GEA 原书中的问题与时效
- 14.5.1.4 以 Havok 的
hkpRigidBody和运动类型为例,属于旧 Havok Physics API 时代的写法;14.5.3.5 的 Havok Destruction、14.6 中 DMM 的例子也偏旧。原理仍然适用。 - 14.5.3.2 "In the Unreal engine… physical materials" 没有指明版本,引用的是 [339]。
- 示例循环与时间步建议自相矛盾:14.5.2.2 的循环末尾
dt = calcDeltaTime();,下一轮把这个实测的可变dt直接传给g_physicsWorld->step(dt);而同节紧接着建议物理用恒定的 1/30 或 1/60 秒步长、不要跟着实际帧率调整步长。按书中建议,step()应该收到固定常量。常见做法是"固定物理步长 + 时间累加器":把实际帧时间累加起来,每够一个固定步长就调用一次step(fixedDt),剩余部分用于渲染插值。这样掉帧时物理既不变慢也不改变步长(Glenn Fiedler, "Fix Your Timestep!", Gaffer On Games, 2004)。GEA 的建议是"掉帧时让物理在视觉上变慢",与这种做法不同,两者并列供参考。
出处
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 14 章 14.5 节(14.5.2.1 除外)、14.6 节
- Physics Asset Editor / Physics Bodies
- Physics in Unreal Engine