概述:整合物理引擎至游戏
单独的碰撞/物理引擎本身不能直接使用。碰撞/物理引擎需要整合至游戏引擎才能发挥作用。
Cre:Unity可以设置
Physics.autoSyncTransforms
当游戏对象位置定向改变时自动同步到物理系统。
Cre:Unity还可以手动调用Physics.SyncTransforms()
手动同步位置变换到物理引擎去。
1.连接游戏对象和刚体
碰撞/物理世界的刚体和碰撞体,只不过是一些抽象的数学描述。要在游戏中利用它们,需要把它们和游戏世界的逻辑对象——游戏对象建立连接。
- 三种可能的情况:
- 零个刚体(不含刚体的游戏对象完全无任何碰撞表示方式,常用于不可交互和到达的装饰性对象。有些需要手工处理碰撞的对象也适用于这种方式。)
- 一个刚体(大多数简单游戏对象只需单个刚体表示。刚体的位置定向完全匹配游戏对象的位置定向,碰撞形状会尽量逼近游戏对象的外观表示。)
- 多个刚体(有些复杂的游戏对象由物理世界中多个刚体表示,例如角色或者载具等多个固体组成的对象。每个刚体位置定向对应至其中一个关节的位置定向。)
从关节到刚体的映射不一定是一对一的,有些关节完全由动画系统控制,有些则连接至刚体。(CRE:有时刚体也受动画系统控制或者影响)
游戏对象和刚体之间的连接,当然必须由引擎管理。通常每个游戏对象会管理自己的刚体,需要时可以在物理世界中创建或者移除刚体,并维护刚体位置和游戏对象或其关节位置的联系。
对于多个刚体的复杂游戏对象,可以使用某种包裹类(wrapper class)管理它们。这样可以避免游戏对象直接管理一组刚体的细节。
CRE:Unity使用游戏对象来对应至关节,刚体只对应至游戏对象而不是直接对应关节。(所以应该不存在一个游戏对象对应多个刚体)
物理驱动刚体:
如果游戏含有刚体动力学系统,那么我们会假设游戏中至少有一些对象完全由模拟驱动。这些对象称为物理驱动对象。例如瓦砾碎片等。
物理驱动刚体连接其游戏对象的方式,是通过step模拟后,向物理引擎查询刚体的位置定向,之后把这个位置定向施于整个游戏对象、游戏对象某关节、或者游戏对象中的其他数据结构。
游戏驱动刚体:
游戏世界的某些物体需要以非物理方式移动,可以由动画或者样条路径或者玩家控制。我们希望这些物体参与碰撞检测,但又不希望物理系统对这些物体有任何影响。多数物理SDK支持这种类型物体,一般称为游戏驱动刚体。
Havok将其称为”受关键帧控制(keyframed)”的刚体。
游戏驱动物体不受重力影响,物理系统把它们当作无穷质量的物体(质量倒数为零)。无穷质量保证力和力矩无法改变其状态。
要在物理世界移动游戏驱动刚体,不能简单地在每帧设置其位置定向以配合游戏对象位置。因为这样会产生不连贯性,例如穿插时无法获得动量做出彭住哪个决议。因此通常会使用冲量移动游戏驱动刚体,以时间向前积分就能令刚体在时步末到达所需位置。要刚体停下来时需要将速度归零。
Cre:Unity中刚体的IsKinematic类型?
固定刚体:
为了模拟游戏世界的静态物体,多数物理SDK会提供一种特别的刚体,称为固定刚体(fixed body)。固定刚体的行为如同游戏驱动刚体,但固定刚体不参与动力学模拟,实际上就是只有碰撞的刚体。
固定刚体能大幅提升多数游戏的性能,特别是大多数物体都是静态物体的游戏。
Cre:对应Unity中没有Rigidbody组件只有Collider类组件的游戏对象??
2.更新模拟
物理模拟需要定期更新。通常每帧一次。
Cre:不一定是每帧更新一次,由游戏循环的设计所决定。
- 更新模拟的意义:
不仅设计步进模拟,也需要维持游戏对象和其刚体的联系。如果游戏需要对任何刚体施加力或者冲量,也必须每帧进行。
- 完整的物理模拟更新步骤:
Ⅰ.更新游戏驱动刚体
更新物理世界中所有游戏驱动刚体的变换,令这些变换匹配相连的游戏世界中的游戏对象或者关节变换。
CRE:Unity中的FixedUpdate中移动刚体之后的步骤?
Ⅱ.更新Phantom
在物理步进之前,需要更新所有phantom的位置,那么执行碰撞检测时这些phantom就处于合适位置。
Ⅲ.施加力或者冲量,并调整约束
对运动方程进行数值积分,求出所有刚体的下一物理状态。执行碰撞检测算法,以求出物理世界中要增加或者删减的刚体接触信息。碰撞决议。施行约束。
Ⅳ.Phantom查询
在物理步进之后,可读取Phantom形状的接触信息。
注意事项:
投射理论上可以置于游戏循环中任何位置,但是最好置于步进模拟之后。
安排碰撞查询的时间有多种选择,可以选择一种或者全部选择。可以在物理步进之前基于前一帧的信息做碰撞查询。也可以在物理步进之后才执行某些查询,但是出结果可能较晚。
3.多线程更新
可以使用单线程更新,也可以使用多线程更新。多线程的模拟更新,有多种构造游戏循环的可行办法。
另一线程上运行物理:
一个选择是在专用的线程上运行物理引擎。但是需要线程同步(thread synchronization)来解决竞争条件(race condition)问题。
Race Condition是指两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
线程同步由互斥锁(mutex)、信标(semaphore)或者临界区域(critical section)实现。
线程同步代价高昂,应该尽量减少线程同步点,物理引擎至少需要两个同步点,一个容许启动该帧的物理模拟(在更新所有游戏驱动物体之后),另一个是在模拟完成后通知主线程(容许主线程查询物理驱动刚体信息)。
降低同步点的数目的策略之一就是使用命令队列(command queue)做线程间通信。首先,主线程锁定一个临界区域,再写一些命令至队列,然后尽快解锁。物理引擎遇到合适的时机,就从队列中取得下一批命令,这时也需要锁定临界区域,以确保主线程不会同时覆写正在读取的队列。
Jason:使用碰撞查询(Raycast等)会使问题更复杂。为了令多个线程能同时存取碰撞/物理世界,一些物理引擎(例如Havok)会容许世界分别以读取或者写入模式进行锁定。那么碰撞查询就可以在游戏循环任何时间执行(读取模式),除非当时物理世界正在用写入模式更新。
分叉汇合架构:
使用fork/join架构的好处是能从本质上消除线程同步问题。
主线程执行至需要步进物理时,我们把步进的过程fork成独立的线程(最理想是每个核配置一个线程)。当所有线程完成工作后整理合并结果。
多数物理SDK(例如Havok和PhysX)都使用碰撞岛概念把刚体分组,此设计很适合fork/join架构,每个岛都可以动态分布到可用线程上。
作业模型:
如果SDK容许模拟步的个别阶段独立执行,那么就可以用作业模型。作业模型能令每个阶段在最方便的时刻启动,然后主线程在等待物理引擎工作时执行其他不相关工作。
Jason:碰撞查询通常是以批次进行的,在游戏循环的几个特选的时间点进行。然而,采用作业模型时,查询可以任何时刻启动,不需要批次执行。
4.应用示例
抛射体:
最简单的实现方式是射线检测。发射的那一帧就判断击中了什么物体,并立即做处理。
射线检测不同呈现出抛射物的飞行路径和飞行时间。所以可用刚体模拟抛射物。
有时候需要完全手动管理抛射物运动,即预先计算好弹道,然后令抛射物沿着弹道前进。弹道甚至可以在游戏画面显示出来。
联机游戏避免在客户端模拟比较重要的会影响游戏结果的刚体。
可破坏物体:
一般会制作一个未破坏的渲染和碰撞版本,作为单独实体。当物体开始破裂时,就用破坏版本替代此模型。
其他情况下,可以一直使用已分割模型。适用于砖墙等堆叠类型。
角色机制:
人形角色或者动物角色的移动太过复杂,通常不能用力或冲量控制其移动。取而代之通常会使用一组游戏驱动的胶囊形刚体模拟角色,每个刚体连接至骨骼中的关节。
为了在游戏世界移动角色,多数游戏会使用球体或者胶囊体投射,往移动方向进行探测。碰撞会使用手动方式求解,可以实现较好的上下坡和上下阶梯动作。
Jason:Havok提供一个角色控制系统,使用胶囊体phantom模拟。Havok为每个角色维护一个碰撞接触流形即降噪后的接触平面集合,用于每帧判断角色最优移动,调整动画。
摄像机碰撞:
大多数第三人称游戏摄像机会追随角色,玩家可以操控摄像机旋转。这类游戏的要点是避免摄像机穿透场景中其他物体。在摄像机碰撞物体之前,摄像机系统会以一些方式调整摄像机的位置定向。
大多数摄像机碰撞系统,基本原理是用一个球形phantom包围虚拟摄像机,或者用球体投射查询检测是否和物体产生碰撞。
整合布娃娃:
最基本的布娃娃效果就是角色失去知觉后由物理系统模拟关节刚体的运动。
- 近年的一些布娃娃技术:
富动力约束的布娃娃。即动画系统等可以间接地控制布娃娃刚体的运动。
失去知觉的角色重新站起。实现此功能需要用某种方式搜寻合适的站起动画。
其他:
- 变形:
随着硬件和算法的发展,部分物理引擎开始提供对可变形体的支持。
- 布料模拟:
布料可以建模为刚性弹簧连接的一堆质点。布料模拟非常难以做好,会有很多难点。
- 毛发:
毛发的实现方式有很多。
- 水面模拟和浮力:
自然的水面运动通常只是一个渲染效果,不会影响物理模拟。
但是物理模拟水面和可交互水面也有很多研究。
- 通用流体动力学模拟:
Jason:一个活跃的研究课题,可能最终以某种形式进入主流物理引擎。
(END)