大家好,这里是七七,今天开始更新物理引擎相关的优化部分了,本文介绍的是物理引擎内部工作情况。
Unity技术有两种不同的物理引擎:用于3D物理的Nvidia的PhysX和用于2D物理的开源项目Box2D。然而,Unity对它们的实现是高度抽象的,从通过主Unity引擎配置的更高级别Unity API的角度来看,两个物理引擎解决方案以功能相同的方式运行。
无论是哪种情况,对Unity的物理引擎了解的越多,就越能理解可能的性能增强。本文将介绍一些关于Unity如何实现这些系统的理论。
物理引擎通常是在时间按固定值前进的假设下运行的,Unity的两个物理引擎也都以这种方式运行。每个迭代称为时间步长。物理引擎将只使用特定时间值来处理每个时间步长,这与渲染上一帧所花费的时间无关。该时间步长在Unity中称为固定更新的时间步长,默认值为20ms。
注意:由于体系结构(浮点值的表示方式)的不同以及客户端之间的延迟,如果物理引擎使用可变的时间步长,就很难在两台不同的计算机之间产生一致的碰撞和力的结果。这样物理引擎往往会在多人的客户端之间或录制的重播期间生成不一致的结果。
固定的更新在物理引擎执行自己的更新之前处理,而这两者之间的联系是不可分割的。这个过程开始于确定是否已经过了足够的时间来开始下一个固定的更新。一旦确定了这一点,则结果将有所不同,这取决于自上次固定更新以来经过的时间。
如果经过了足够的时间,则固定更新的处理将调用在场景中所有激活的MonoBehaviour中定义的FixedUpdate()回调,接着处理与固定更新相关的任何协程(特别是那些生成WaitForFixedUpdate的协程)。注意,对于在这两个过程中调用的方法,没有执行顺序的保证,所以不应该在这个假设下编写代码。一旦这些任务完成,物理引擎就可以开始处理当前的时间步长,并调回任何必要的触发器和碰撞器。
相反,如果上次固定更新以来经过的时间太短(小于20ms),则跳过当前的固定更新,并且之前列出的所有任务不会再当前迭代期间处理。此时,输入、游戏逻辑和渲染将正常进行。完成此活动后,Unity将检查是否需要处理下一个固定更新。
在高帧率下,渲染更新可能会在物理引擎获得自身更新机会之前完成多次更新。这个过程在运行时不断重复,使固定的更新和物理引擎比渲染具有更高的优先级,同时也强制物理模拟具有固定的帧率。
提示:为了确保对象在固定更新之间平稳移动,物理引擎根据下一次固定更新之前的剩余时间,在处理当前状态之后,在上一个状态和应处于的状态之间对每个对象的可见位置进行插值。这种插值可以确保对象的移动非常平稳,尽管它们的物理位置、速度等更新的频率低于渲染帧率。
FixedUpdate()回调适用于任何期望独立于帧率的游戏行为。AI通常在固定的更新中计算,因为如果假设一个固定更新的频率,会更容易开发。
需要注意的是,如果自上次固定更新(例如游戏暂时卡顿)以来已经过了很长时间,那么固定更新将继续在相同的固定更新循环中计算,直到物理引擎赶上当前时间。如果上一帧画了100ms用于渲染(例如,一个突然的CPU峰值导致主线程阻塞了很长时间),那么物理引擎将需要更新5次。由于默认固定更新的时间步长为20ms,在再次调用Update()之前还需要调用5次FixedUpdate()。当然,如果在这5次固定更新时有很多物理活动需要处理,例如总共花费了超过20ms处理它们,那么物理引擎将继续调用第6次更新。
因此,在物理活动较多时,物理引擎处理固定更新的时间可能比模拟的时间要长。例如,如果用30ms来处理一个固定的更新,模拟20ms的游戏,它就已经落后了,需要它处理更多的时间步长来尝试和跟上,但这可能会导致它落后得更远,需要它处理更多的时间步长,等等。在这些情况下,物理引擎永远无法摆脱固定的更新循环,并允许另一帧进行渲染,这个问题通常称为死亡螺旋。但是,为了防止物理引擎在这些时刻锁定游戏,存在允许物理引擎处理每个固定更新循环的最长时间,则它将停止并放弃进一步的处理,直到下一次渲染更新完成。这种设计允许渲染管线至少将当前状态进行渲染,并允许用户输入以及游戏逻辑在物理引擎出现异常的罕见时刻做出一些决策。
该设置可以通过Edit|Project Settings|Time|Maximum Alowed Timestep来访问。
当物理引擎以给定的时间步长处理时,它必须移动激活的刚体对象,检测新的碰撞,并调用相应对象的碰撞回调。Unity文档明确指出,应该在FixedUpdate()和其他物理回调中处理对刚体对象的更改,原因正是如此。这些方法与物理引擎的更新频率紧密耦合,而不是游戏循环的其他部分,如Update()。
这意味着,诸如FixedUpdate()和OnTriggerEnter()的回调函数能够安全更改Rigidbody的位置,而诸如Update()和对WaitForSeconds或WaitForEndOfFrame的协程却不能。忽略这一建议可能会导致意想不到的物理行为,因为在物理引擎有机会捕获和处理所有这些对象之前,可能会对同一个对象进行多次更改。
对Update()回调中的对象应用力或脉冲而不考虑这些调用的频率是非常危险的。例如,在玩家按住一个键时,给Update功能应用10牛顿的力,会导致两个不同设备之间的合成速度完全不同于在固定更新中执行相同的操作。事实上,不能依赖Update()调用的次数是一致的。但是,在FixedUpdat()回调中这样做会更加一致。因此,必须确保在适当的回调中处理所有与物理引擎相关的行为,否则就可能引入一些令人困惑,很难重现的游戏漏洞。
从逻辑上讲,在任何给定的固定更新迭代中花费的时间越多,在下一次游戏逻辑和渲染过程中花费的时间就越少。由于物理引擎几乎没有任何工作要做,而且FixedUpdate()回调有很多时间来完成它们的工作,因此大多数情况下这会导致一些小的、不明显的后台处理任务。然而,在某些游戏中,物理引擎可能在每次固定更新期间执行大量计算。这种物理处理时间上的瓶颈会影响帧率,导致它在当物理引擎负担越来越大的工作负载时,帧率急剧下降。基本上,渲染管线将尝试正常进行,但每当需要进行固定更新时(物理引擎处理时间很长),渲染管线在帧结束之前几乎没有时间生成画面,会导致突然停顿。物理引擎达到允许的最大时间步长,会导致过早停止的视觉效果。所有这些加在一起会产生非常糟糕的用户体验。
因此,为了保持平滑、一致的帧率,需要通过最小化物理引擎处理任何给定时间步长所需的时间,来为渲染释放尽可能多的时间,这适用于最佳情况(没有移动)和最坏情况(所有对象同时与其它对象发生碰撞)。可以在物理引擎中调整一些与事件相关的特征和值,以避免这些性能缺陷。
在Unity中,术语"静态"和"动态"又一个相当极端的命名空间冲突。静态通常意味着所讨论的对象或处理不移动、保持不变或只存在于一个位置,而动态则意味着相反,对象或处理倾向于改变或移动。然而要记住,术语"静态"和"动态"的用法在每种情况下都不同。
动态碰撞器只意味着GameObject包含Collider组件和Rigidbody组件。通过将Rigidbody添加到Collider所附加的相同对象上,物理引擎会将该碰撞器视为带有包围物理对象的立体对象,它会对外部的力和与其他Rigidbody的碰撞体作出反应。如果一个动态碰撞器与另一个动态碰撞器发生碰撞,它们都会基于牛顿运动定律做出反应。
也可以使用没有附加Rigidbody组件的碰撞器,这种称为静态碰撞器。这种碰撞器有效地起到了无形屏障的作用,动态碰撞器可以撞到这些屏障,但是静态碰撞器不会做出响应。从另一个角度来看,就是把没有Rigidbody组件的物体想象成具有无穷大的质量。因此,静态碰撞器非常适合用作全局屏障和其他不能移动的障碍物。
物理引擎自动将动态碰撞器和静态碰撞器分为两种不同的数据结构,每种结构都经过优化以处理现有碰撞器的类型。这有助于简化未来的任务,例如,解析两个静态碰撞器之间的碰撞和脉冲。
Unity中的碰撞检测有3种设置,可以在Rigidbody组件的Collision Detection属性中设置Discrete(离散)、Continuous(连续)和ContinuousDynamic(连续动态)。
Discrete设置可以实现离散碰撞检测,有效地根据物体的速度和经过的时间,在某个时间步长将对象传送一小段距离。一旦所有对象都被移动了,物理引擎就会对所有重叠执行便捷进行立体检查,将它们视为碰撞,并根据它们的物理属性和重叠方式来处理它们。如果小对象移动得太快,此方法可能会有丢失碰撞的风险。
其余的两个设置都将启用连续碰撞检测,其工作方式是从当前时间步长的起始和结束位置,并见哈这个时间段中是否有任何碰撞。这降低了错过碰撞的风险,生成了更景区的模拟,但代价是CPU的开销显著高于离散碰撞检测。
Continuous设置尽在给定碰撞器和静态碰撞器之间用连续碰撞检测。同一碰撞器与动态碰撞器之间的碰撞仍将使用离散碰撞检测。
同时,ContinuousDynamic设置使碰撞器能够与所有静态和动态碰撞器进行连续碰撞检测,其在资源消耗方面最大。
Unity中有4种不同类型的3D碰撞器,其性能成本从最小到最大依次为球体(Sphere)、胶囊体(Capsule)、立方体(Box)、网格(Mesh)。
前三个碰撞器类型通常称为基础类型。包含非常特殊的形状,尽管它们通常可以在不同方向缩放以满足某些要求。网格碰撞器可以根据指定的网格自定义为特定形状。还有3种类型的二维碰撞器:圆(Circle)、方框(Box)和多边形(Polygon),在功能上分别与球形、立方体和网格碰撞器相似。以下所有信息基本上都可以转换为等效的二维形状。
提示:也可以在Unity中生成3D圆柱体,但这只是它的图形表现。自动生成的圆柱体使用胶囊体碰撞器表示其物理保卫面积,这可能不会产生预期的物理行为。
另外,有两种不同的网格碰撞器:Convex(凸的)和Concave(凹的)。两者的不同之处在于,凹形形状至少具有一个大于180度的内角,如图所示:
?提示:区分凹形和凸形的一个简单方法是凹形至少有一个凹陷。
两种网格碰撞器类型都使用相同的组件(MeshCollider组件),这种网格碰撞器类型是通过切换Convex复选框选项生成的。启用此选项将允许对象与所有基本形状(球形、长方体等)以及其他启用Convex的网格碰撞器碰撞
此外,如果为凹形的网格碰撞器启用了Convex复选框,则物理引擎将自动简化该网格碰撞器,生成的碰撞器具有能将其包围的最接近的凸形。
在上图中,如果导入右侧的凹形网格并启用Convex复选框,它将生成一个更接近左侧凸形形状的碰撞器。在这两种情况下,物理引擎都将尝试生成一个碰撞器,该碰撞器与附加的网格的形状匹配,上限为255个顶点。如果目标网格的顶点数超过此值,则在网格生成过程中会引发错误。
碰撞器组件还包含IsTrigger属性,允许将他们视为非物理对象,但当其他碰撞器进入或离开它们时仍调用物理时间。这些称为触发体积。通常,当一个碰撞器接触、保持接触、或停止接触时,分别会调用OnCollosionEnter()等三个回调,但当碰撞器用作出发体积时,将调用OnTriggerEnter()等三个回调。
注意:由于处理物体间碰撞的复杂性,凹形网格碰撞器不能是动态碰撞器,只能用作静态碰撞器或触发体积。如果试图将Rigidbody组件添加到凹形网格碰撞器中,Unity将完全忽略它。
提示:如果真的需要将凹形网格碰撞器作为Rigidbody组件,则解决方案是将对象分割成独立的凸形网格碰撞器的组合。例如,想利用两个凸形来组合一个L形刚体。不幸的是,因为这是一个微妙的决定,所以没有自动的方法来实现,需要手动执行这样的分解。
物理引擎具有一个碰撞矩阵,该矩阵定义允许哪些对象与哪些对象发生碰撞。当处理边界体积重叠和碰撞时,物理引擎家那个自动忽略不适合此矩阵的对象。这节省了碰撞检测阶段的物理处理,还允许对象彼此移动而不发生任何碰撞。
碰撞矩阵可以通过Edit | Project Settings | (Physics/Physice2D) | Layers Collision Matrix访问。
碰撞矩阵系统通过Unity的层(Layer)系统工作。矩阵表示层与层之间的组合,启用复选框意味着在碰撞检测阶段将检查这两个层中的碰撞器。
要注意的是,对于整个项目,总共只能有32个层。
每一个现代物理引擎都有一个共同的优化技术,即静止物体的内部状态从活动变为休眠。当Rigidbody处于休眠状态时,在固定的更新过程中,处理器几乎没有时间来更新对象,直到它被外力或碰撞事件唤醒。
用于确定静止状态的测量值,在不同的物理引擎中往往会有所不同,可以使用Rigidbody的线速度和角速度、动能、栋梁或其他一些物理属性来计算。Unity的两个物理引擎都是通过评估物体的质量归一化动能来工作的,这基本上可以取决于物体速度平方的大小。
如果物体的速度在短时间内没有超过某个阈值,那么物理引擎将假设物体在经历新的碰撞或施加新的力之前不再需要再次移动。在此之前,休眠对象将保持其当前位置。可以在Edit | Project Settings | Physice | Sleep Threshold下修改阈值,还可以从Profiler窗口的Physics Area中获取活动Rigidbody对象的总数。
物理引擎的另一个常见特征是能够将射线从一个点投射到另一个点,并用路径中的一个或多个对象生成碰撞信息,这就是所谓的射线投射。通过射线投射来实现一些游戏机制是很常见的,比如射击,其实现方式通常是执行从玩家到目标位置的射线投射,并在其路径中找到任何符合要求的目标。
还可以通过Physics.OverlapSphere()检查在空间中固定点的有限距离内获得的目标列表,这通常用于实现效果区域的游戏功能,如手榴弹爆炸,甚至可以用Physics.SphereCast()和Physics.CapsuleCast()在空间中向前投射整个对象。这些方法通常用来模拟宽激光束,或者只是确定什么东西在移动角色的路径中。
物理错误通常分为两类:本来不应该碰撞的一对对象碰撞了;本来应该碰撞的没碰撞,在碰撞发生之后,发生了意想不到的事情。前一种情况往往更容易调试,通常是由于碰撞矩阵中的错误,射线投射中使用的层不正确,或者对象碰撞器的大小或形状错误。后一种情况往往更难解决,因为要获得以下3条消息:
- 确定哪个碰撞对象导致了问题
- 在解决之前确定碰撞条件
- 重现碰撞
获得了这3条信息中的任何一条都会使解决方案更容易,但这些信息在某些情况下都很难获得。
Profiler在Physics和Physics(2D)区域提供了一些测量信息,这是相当有用的,可以得到CPU活动在不通过类型隔离的所有刚体和刚体组上花费的量,这些类型包括动态碰撞器、静态碰撞器、运动对象、触发体积、约束和触点。
Physics 2D区域包含了更多的信息,比如睡眠和活动刚体的数量,以及处理时间步长的时间。在这两种情况下,详细的细分试图提供了更多的信息。这些信息有助于关注物理性能,但它并不能指出在物理行为中出现错误时发生了什么。
一个更适合帮助调试物理问题的工具是Physics Debugger,它可以通过Window | Physics Debugger打开。这个工具有助于从Scene窗口中过滤出不同类型的碰撞器,从而更好地了解哪些对象相互碰撞。当然,这对确定问题的条件和复现问题没有太大帮助。