Unity游戏优化[第二版]学习记录5
- 第5章 加速物理
- 一、物理引擎的内部工作情况
- 1、物理和时间
- 1)最大允许的时间步长
- 2)物理更新和运行时变化
- 2、静态碰撞器和动态碰撞器
- 3、碰撞检测
- 4、 碰撞器类型
- 5、 碰撞矩阵
- 6、 Rigidbody激活和休眠状态
- 7、 射线和对象投射
- 8、 调试物理
- 二、物理性能优化
- 1、场景设置
- 1)缩放
- 2)位置
- 3)质量
- 2、适当使用静态碰撞器
- 3、恰当使用触发体积
- 4、优化碰撞矩阵
- 5、首选离散碰撞检测
- 6、修改固定更新频率
- 7、调整允许的最大时间步长
- 8、最小化射线发射和边界体积检查
- 9、避免复杂的网格碰撞器
- 10、避免复杂的物理组件
- 11、使物理对象休眠
- 12、修改处理器迭代次数
- 13、优化布娃娃
- 1)减少关节和碰撞器
- 2)避免布娃娃间碰撞
- 3)更换、禁用或移除不活跃的布娃娃
- 14、确定何时使用物理
第5章 加速物理
一、物理引擎的内部工作情况
Unity技术上有两种不同的物理引擎:用于3D物理的Nvidia的PhysX和用于2D物理的开源项目Box2D。
1、物理和时间
物理引擎通常是在时间按固定值前进的假设下运行的,Unity的两个物理引擎也都以这种方式运行。每个迭代都称为时间步长。物理引擎将只使用非常特定的时间值来处理每个时间步长,这与渲染上一帧所花费的时间无关。该时间步长在Unity中称为Fixed Update Timestep,它的默认值设置为20毫秒(每秒50次更新)
注意:由于体系结构(浮点值的表示方式)的不同以及客户端之间的延迟,如果物理引擎使用可变的时间步长,很难在两台不同的计算机之间产生一致的碰撞和力的结果。这样会导致不同的客户端之间或在录制重播期间生成不一样的结构。
1)最大允许的时间步长
如果用30毫秒来处理一个固定的更新,模拟20毫秒的游戏,就需要处理更多的时间步长来尝试和跟上,但这可能导致它落后得更远,需要处理更多的时间步长。这种情况下,物理引擎永远无法摆脱固定的更新循环,并允许另一帧进行渲染,这个问题通常称为死亡螺旋。
为了防止物理引擎在这些时刻锁定游戏,存在允许物理引擎处理每个固定的更新循环的最长时间,此阈值称为允许的最大时间步长(Maximun Allowed Timestep),如果当前一批固定更新的处理时间太长,则将它停止并放弃进一步的处理,直到下一次渲染更新完成。这种设计允许渲染管线至少将当前状态进行渲染,并允许用户输入以及游戏逻辑在物理引擎出现异常的罕见时刻做出一些决策。
该设置可以通过Edit | Project Settings | Time | Maximun Allowed Timestep来访问
2)物理更新和运行时变化
当物理引擎以给定的时间步长处理时,它必须移动激活的刚体对象,检测新的碰撞,并调用相应对象的碰撞回调。Unity文档明确指出,应在FixedUpdate()和其它物理回调中处理对刚体对象的更改,因为这些方法与物理引擎紧密耦合,而不是游戏循环的其它部分。
这意味着,FixedUpdate()和OnTriggerEnter()等回调函数是安全更改Rigidbody的方法,而Update()和WaitForSeconds或WaitForEndOfFrame等协程方法则不是。否则,会导致两个设备之间的物理效果不一致。
2、静态碰撞器和动态碰撞器
动态碰撞器:包含Collider类组件和Rigidbody组件的GameObject。会对外部的力(例如重力)或者碰撞作出反应
静态碰撞器:包含Collider类组件,但不包含Rigidbody组件的GameObject。动态碰撞器可以与静态碰撞器发生碰撞,但是只有动态碰撞器会做出反应,静态碰撞器不会移动。
物理引擎自动将动态碰撞器和静态碰撞器分为两种不同的数据结构,每种数据结构都经过优化以处理现有碰撞器的类型。
3、碰撞检测
Unity的碰撞检测有3种设置:
Discrete(离散):在每个时间步长将对象按速度移动一小段距离,若下一次运动后两个问题重叠,物理引擎会对所有重叠的物体执行边界立体检查,将它们视为碰撞。但是,如果小对象移动得太快,此方法可能会有丢失碰撞的风险。
Continuous(连续):从当前时间步长的起始和结束位置插入碰撞器,并检查这个时间段种是否有任何碰撞。即在一个时间步长中多次进行检测,缩小物体移动的距离,达到更精确的计算效果,但代价是开销显著高于离散碰撞检测。另外,此设置仅在给定碰撞器和静态碰撞器之间启用。
ContinuousDynamix(连续动态):与连续碰撞检测相同,但是能与所有静态和动态碰撞器进行检测,因此,其在性能消耗方面也最昂贵。
4、 碰撞器类型
3D碰撞器(4种):
球体碰撞器(Sphere)
胶囊体碰撞器(Capsule)
立方体碰撞器(Cube)
网格碰撞器(Mesh)
2D碰撞器(3种):
圆型碰撞器(Circle)
方框碰撞器(Box)
多边形碰撞器(Polygon)
注意:Unity种生成的3D圆柱体,实际使用的是胶囊体碰撞器,这可能不会产生预期的物理行为
5、 碰撞矩阵
物理引擎具有一个碰撞矩阵,该矩阵定义允许哪些对象与哪些其他对象发生碰撞。
碰撞矩阵系统通过Unity的层(Layer)系统工作。矩阵表示层与层每个可能的组合。注意,不可能只允许两个对象中的一个对碰撞做出响应。如果一个层可以与另一个层碰撞,那么它们都必须对碰撞做出响应。但是,静态碰撞器是个例外,因为它们不允许对碰撞进行物理响应(尽管它们仍然收到OnColiision…回调)
对于整个项目,总共有32个层(因为物理引擎使用32位掩码来确定层间冲突的机会),无论出于什么原因,如果32个层对于项目来说还不够,就可能需要找到巧妙的方法来重用层或删除不必要的层
6、 Rigidbody激活和休眠状态
休眠状态:如果物体的速度在短时间内没有超过某个阈值(如截图中框住的属性即位阈值),那么物理引擎将假设物理在经历新的碰撞或施加新的力之前不再需要再次移动。
请注意,休眠对象不会完全从模拟中删除。如果移动的Rigidbody接近休眠对象,则它必须执行检查,以查看附近的对象是否与之碰撞,并将其重新引入模拟进行处理。
7、 射线和对象投射
物理引擎的另一个常见特征是能够将射线从一个点投射到另一个点,并用路径中的一个或多个对象生成碰撞信息。这就是所谓的射线投射。
还可以通过Physics.OverlapSphere()检查空间中向前投射的有限距离内获得目标列表。这通常用于实现效果区域的游戏功能,如手榴弹或火球爆炸。甚至可以使用Physics.SphereCast()和Physics.CapsuleCast()在空间中向前投射整个对象。这些方法通常用于模拟宽激光束,或者只是确定什么东西在移动角色的路径中。
8、 调试物理
物理错误通常分为两类:
本来不应该碰撞的一对对象碰撞了;
本来应该碰撞的一对对象没有碰撞,但是在碰撞发生之后,发生了意想不到的事情。
前一种情况通常更容易调试,这通常是由于碰撞矩阵中的错误,射线投射中使用的Layer不正确,或者对象碰撞器的大小或者形状错误。后一种情况往往更难解决,因为有3大问题:
· 确定哪个碰撞对象导致了问题
· 在解决之前确定碰撞的条件
· 重现碰撞
Profiler在Physics和Physics(2D)区域(分别针对3D和2D物理)提供了一些测量信息,这是相当有用的。可以得到CPU活动在与不同类型隔离的所有刚体和刚体组件上花费的量,这些类型包括动态碰撞器、静态碰撞器、运动对象、触发体积、约束和触点。
一个更适合帮助调试物理问题的工具是Physics Debugger,它可以通过Window | Physics Debugger打开。这个工具可以帮助从Scene窗口中过滤出不同类型的碰撞器,从而更好地了解哪些对象相互碰撞。
二、物理性能优化
1、场景设置
首先,可以将许多最佳实践应用到场景中,以提高物理模拟的一致性。请注意,这些技术中并不一定都会提高CPU或内存的使用率,但它们会降低物理引擎的不稳定性。
1)缩放
应该尽可能地使游戏世界中所有物理物体的缩放接近(1,1,1)。默认情况下,Unity模拟的游戏玩法相当于地球表面。地球表面的重力为9.81米/秒²,因此默认重力值为-9.81以匹配地球的重力。如果缩放过大会导致重力移动物体的速度比预期的要慢得多。如果所有物体都放大了5倍,那么重力就会减弱5倍,反之亦然。
可以通过Edit | Project Settings | Physics/Physics 2D | Gravity修改重力强度。请注意,任何浮点运算在数值接近于0时都会更精确,因此,如果一些对象的比例值远高于(1,1,1),即使它们与隐含的世界缩放匹配。仍然会有不稳定的物理行为。
2)位置
同样,保持所有对象在世界空间的位置接近(0,0,0),将具有更好的浮点数精度,提高模拟的一致性。空间模拟器和自由运行游戏试图模拟非常大的空间,通常使用一个技巧,要么秘密地将玩家传送回世界的中心,要么固定它们的位置,在这种情况下,空间的任何一个体积都被划分,这样物理计算总是用接近0的值来计算。或者移动其它对象来模拟旅行,而玩家的移动只是一种错觉。
大多数游戏都没有浮点不精确的风险,因为大多数游戏的关卡往往持续10到30分钟左右,这不会给玩家足够的世界进行荒诞的长途旅行,但如果在整个游戏过程中处理超大的场景或者异步加载场景,直到玩家走了数万米,就可能会注意到它们走到远处产生的一些奇怪的物理行为。
3)质量
传统上,值为1.0的质量表示1kg,但是可以指定一个人的质量是1.0(130kg),在这种情况下,汽车的质量值是10.0(1300kg),物理碰撞的解决方法与期望的类似。最重要的部分是质量的相对差异,这样,这些物体之间的碰撞看起来是可信的,而不会对物理引擎施加过大的压力。浮点精度也是一个问题,所以我们不想使用太荒谬的超大质量值。
理想情况下,质量值保持在1.0左右,并确保最大相对质量比在100左右。如果出现两个物体的碰撞质量比这个大得多,那么巨大的动量差会由于冲量而变成突然的、巨大的速度变化,导致一些不稳定的物理现象和浮点精度的潜在丢失。
请注意,地球中心的重力对所有的物体都有同等的影响,不管它们的质量如何,因此,无论将质量属性值1.0视为橡胶球还是军舰的质量都不重要,不需要调整重力来补偿。然而,重要的是给定物体在下落时所受的空气阻力。为了获得逼真的行为,可能需要为这些对象自定义drag属性,或者基于每个对象自定义重力。例如,可以禁用Use Gravity复选框,并在固定的更新期间应用自己的自定义重力。
2、适当使用静态碰撞器
如果在运行时将新对象引入静态碰撞器数据结构,那么必须重新生成它,类似于为静态批处理调用StaticBatchingUtility.Combine()。这可能会导致显著的CPU峰值。在游戏中避免实例化新的静态碰撞器是至关重要的。
此外,仅移动、旋转或缩放静态碰撞器也会触发此重新生成的过程,也应该避免。如果碰撞器希望在不与其他物体发生物理碰撞的情况下移动,那么应该附加一个Rigidbody,使其成为动态碰撞器,并开启Kinematic标志,此标志防止对象对来自对象间碰撞的外部脉冲做出反应,类似于静态碰撞,但对象仍可以通过其Transform组件或通过施加到其Rigidbody组件上的力(最好在固定更新期间)移动。由于Kinematic对象不会对撞击它的其它物体做出反应,它在运动时会简单地把其它动态碰撞器推开。
正是因为这个原因,玩家角色的物体经常制成Kinematic碰撞器。
3、恰当使用触发体积
可以将物理对象作为简单的碰撞器或触发体积。这两种类型的重要区别是OnCollider…()回调提供了一个Collision对象作为回调参数,它包括诸如精确的碰撞位置和接触法线等有用信息,而OnTrigger…()回调则没有提供这类信息。
因此,不应该尝试使用触发体积对碰撞做出反应,因为没有足够的信息使得碰撞看起来准确。触发体积最适用于在如下情况达到其预期的跟踪目的:当物体进入/离开特定的区域时,例如当玩家停留在熔岩坑时处理伤害,当玩家进入一个建筑时触发一个场景等
如果触发体积碰撞确实需要相交信息,则常见的解决方法是执行以下任一操作:
· 通过将触发体积和碰撞对象的质量中心之间的距离减半,生成粗略估计的接触点(假设它们的大小大致相等)
· 从触发体积中心执行射线发射到碰撞对象的质量中心(当两个对象都是球体时效果最好)
· 创建一个非触发体积对象,给它一个无穷小的质量(这样碰撞对象就不会受到它的影响),并在碰撞时立即摧毁它(因为质量差如此大的碰撞可能会使这个小对象非常活跃)。
当然,每个方法都有它们的缺点,有限的物理精度,碰撞时额外的CPU开销,以及/或者额外的场景设置,或者相对难处理的碰撞代码,但在紧要关头它们可能有用。
4、优化碰撞矩阵
物理引擎的碰撞矩阵定义了物理引擎关心的对象碰撞对。物理引擎简单地忽略了其它每一个对象层对,这使得碰撞矩阵成为最小化物理引擎工作负载的重要途经,因为它减少了每次固定更新必须检查的边界体积的数量,以及在应用程序的生命周期中需要处理的碰撞数量(这将延长移动设备的电池寿命)。
应该对碰撞矩阵中这所有潜在的层组合执行逻辑健全性检查,以查看是否在浪费宝贵的时间检查不必要对象对之间的碰撞。
5、首选离散碰撞检测
离散碰撞检测的消耗相当低,因为只传送一次对象并在附近的对象对之间执行一次重叠检查,在一个时间步长的工作量相当小。
应该支持绝大多数对象采用离散设置,而只在极端情况下使用连续碰撞检测设置。当重要的碰撞经常被游戏世界中比较静态的部分忽略时,应该使用连续设置。最后,只有在希望捕捉快速移动的动态碰撞器对之间的碰撞的情况下才应该使用连续动态设置。
6、修改固定更新频率
在某些情况下,,离散碰撞检测在大范围内可能不够好。也许整个游戏包含着许多小的物理对象,而离散碰撞检测根本无法捕捉足够的碰撞来保持产品质量。然而,将一个连续碰撞检测设置用于所有对象,对性能来说则太过昂贵了。在这种情况下,可以尝试一个选项:可以自定义物理时间步长,通过修改引擎检测固定更新的频率,为离散碰撞检测系统提供更好的捕捉此类碰撞的机会。
可以在编辑器中通过Edit | Project Settings | Time | Fixed Timestep 属性或者脚本代码中通过Time.fixedDeltaTime 属性完成Fixed Update 频率的修改。
7、调整允许的最大时间步长
如果处理物理计算的时间经常超过允许的最大时间步长,那么将导致一些看起来很奇怪的物理行为。由于物理引擎需要在完全处理其整个世界配额之前,尽早退出世界步长计算,因此,刚体似乎会减速或突然停止。在这种情况下,很明显需要从其它角度优化游戏中的物理行为。然而,至少可以确信,阈值将防止游戏在物理处理过程的峰值中完全卡住。
8、最小化射线发射和边界体积检查
如果场景中使用持续的线、射线或区域效果碰撞区域(例如安全激光、持续燃烧的火焰、光束武器等),并且对象保持相对静止,那么使用简单的触发体积就可能更好地模拟它们。
如果不能进行此类替换,且确实需要使用这些方法进行持久的投射检查,那么应该使用层遮罩来最小化每个射线的处理量。如果使用Physics.RaycastAll()方法,这一点尤其重要。
9、避免复杂的网格碰撞器
1)使用更简单的基本碰撞体
大多数形状可以使用3个基本碰撞器中的一个进行近似模拟。
2)使用更简单的网格碰撞器
分配给网格碰撞器的网格不一定需要匹配相同对象的图形表示,可以为网格碰撞器的mesh属性指定一个更简单的网格,该网格为用于图形表示网格的简化版。
10、避免复杂的物理组件
某些特殊的物理碰撞器组件,例如TerrainColiider、Cloth和WheelCollider,在某些情况下比所有基础碰撞器甚至网格碰撞器的消耗都有高上几个数量级。不应该在场景中包含这些组件,除非它们是必要的。例如,如果在玩家永远不会接近的距离内有地形物体,则不应该为该地形添加TerrainCollider。
具有Cloth组件的游戏应该考虑在低质量环境下运行时,在没有这些组件的情况下实例化不同的对象,或者简单地设置布料行为动画。
使用WheelCollider组件的游戏应该尽量少使用Wheel碰撞器。拥有4个以上车轮的大型车辆可以仅使用4个车轮模拟类似的行为,同时模拟附加车轮的图形表示。
11、使物理对象休眠
游戏引擎的休眠特性会给游戏带来一些问题。
首先,每次在模拟中引入新的物理对象,都会导致意外的性能成本。
其次,在运行时修改Rigidbody组件的任何属性,例如mass、drag以及useGravity会重新唤醒对象。如果经常改变这些值,它们将比正常情况更活跃。可以检测它的质量归一化动能(仅使用velocity.sqrMagnitude的值),并在检测到其值非常低时,手动禁用自定义重力。
再次,休眠的物理对象有产生岛屿效应的危险。当大量刚体互相接触,并随着系统动能的降低,逐渐休眠变成岛屿。然而,由于它们依然互相接触,一旦这些对象被唤醒,便会产生链式反应,唤醒周围的所有刚体,这会导致一瞬间大量对应需要重新进入物理模拟,将会产生较大的CPU峰值。甚至,由于对象太近,在对象再次休眠之前,会有大量潜在的碰撞对需要处理。
最好通过降低场景的复杂性,如果无法做到这一点,可以寻找方法来检测岛屿的形成,然后战略性地销毁其中一些,以防止产生大型岛屿。例如,将羊赶进围栏,可以选择玩家将羊移动到位后立即移除,将物体锁定到其最终目的地,减轻物理引擎的工作量,防止形成岛屿问题。
12、修改处理器迭代次数
在物理引擎中,使用关节、弹簧和其他方法将刚体连接在一起是相当复杂的模拟。由于将两个对象连接在一起而产生相互依赖的交互作用,系统必须经常尝试求解必要的数学方程。当物体链的任何一部分的速度发生变化时,需要使用这种多迭代方法来计算精确结果。
因此,必须在限制处理器解决特定情况的最大尝试次数和限制所得结果的准确性之间找到平衡。处理器不应在一次碰撞上花费太多时间。但是最大迭代次数也不应减少得太多,因为它只近似于最终的解决方案,所以它的运行看起来比用更多时间计算的结果更不可信。
处理器运行的尝试最大迭代次数成为Solver Iteration Count,可在Edit | Project Settings | Physics | Default Solver Iterations下修改。在大多情况下,六次迭代的默认值是完全可以接受的。然而,如果游戏包含非常复杂的关节系统,就可能希望增加该次数,以防止任何不稳定的CharacterJoint行为。
13、优化布娃娃
1)减少关节和碰撞器
Unity在GameObject | 3D Object Ragdoll…下提供了简单的布娃娃生成工具。该工具通常创建13个不同的碰撞器并关联关节
但是,只使用奇怪碰撞器,可以大大降低消耗成本,代价是牺牲了布娃娃的真实性。为此,可以输出不必要的碰撞器,手动将角色关节的connectedBody属性重新指定给适当的父关节。
2)避免布娃娃间碰撞
当允许布娃娃与其他布娃娃碰撞时,布娃娃的性能成本呈指数级增长,因为任何关节碰撞都要求处理器计算应用于所有连接到它的关节的合成速度,然后计算每个连接到它们的关节,这样两个布娃娃必须完成多次计算。此外,如果布娃娃的多个部分可能在同一次碰撞中彼此碰撞,则情况会变得更加复杂。
3)更换、禁用或移除不活跃的布娃娃
在某些游戏中,一旦布娃娃到达它的最终目的地,就不再需要它作为一个可交互的对象留在游戏世界中。当不再需要布娃娃时,可以禁用、销毁它,或用更简单的替代品替换它。
通过Rigidbody.IsSleeping()观察每个布娃娃是否休眠,再对它们做相应的处理。
14、确定何时使用物理
提高特性性能最明显的方法时尽量避免使用它。对于游戏中所有可移动的物体,应该确定是否有必要使用物理引擎。
例如,用物理方法检测玩家是否掉入了一个杀戮区,但是游戏非常简单,只存在特定高度的杀戮区,在这种情况下,只需要检查玩家的Y轴位置是否低于某个特定值,就可以完全避免使用物理碰撞器
反过来也是可能的,在某些情况下,可能会通过脚本代码执行大量计算,而脚本代码可以相对简单地通过物理进行处理。