Hero background image

增强物理性能以实现流畅的游戏体验

为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

这是系列文章中的第五篇,解读了您在 Unity 项目中的优化技巧。将其作为在更少资源下以更高帧率运行的指南。一旦您尝试了这些最佳实践,请务必查看系列中的其他页面:

物理可以创造复杂的游戏玩法,但这会带来性能成本。一旦您了解这些成本,您可以调整模拟以适当地管理它们。使用这些技巧保持在目标帧率内,并利用 Unity 内置的物理引擎(NVIDIA PhysX)创建流畅的播放效果。

查看我们为 Unity 6 开发者和艺术家提供的最新优化指南:

检查您的碰撞体

用于物理的网格经过称为烹饪的过程。这准备了网格,以便它可以与物理查询(如射线检测、接触等)一起工作。

MeshCollider有几个CookingOptions来帮助您验证物理网格。如果您确定您的网格不需要这些检查,可以禁用它们以加快烹饪时间。

在每个MeshCollider的CookingOptions中,只需取消选中EnableMeshCleaning、WeldColocatedVertices和CookForFasterSimulation。这些选项对于在运行时程序生成的网格非常有价值,但如果您的网格已经具有适当的三角形,则可以禁用它们。

此外,如果您针对PC,请确保保持Use Fast Midphase启用。这在模拟的中期阶段切换到PhysX 4.1中的更快算法(有助于缩小一小组可能相交的三角形以进行物理查询)。

CookingOptions文档中了解更多。

Unity编辑器中的烹饪选项界面
网格的烹饪选项

使用 Physics.BakeMesh

如果您在游戏过程中以程序方式生成网格,可以在运行时创建Mesh Collider。然而,直接将MeshCollider组件添加到网格会在主线程上烹饪/烘焙物理。这可能会消耗大量CPU时间。

使用 Physics.BakeMesh 准备网格以供 MeshCollider 使用,并将烘焙数据与网格本身一起保存。引用此网格的新 MeshCollider 将重用此预烘焙数据(而不是再次烘焙网格)。这可以帮助减少场景加载时间或后续实例化时间。

为了优化性能,您可以使用 C# 作业系统 将网格烹饪卸载到另一个线程。

有关如何在多个线程中烘焙网格的详细信息,请参阅 此示例

BakeMeshJob 接口
Profiler 中的 BakeMeshJob

调整您的设置

Player Settings 中,尽可能检查 预烘焙碰撞网格。我们还建议检查 碰撞矩阵 设置,以确保玩家和游戏机制对象位于正确的层中。

从不必要的层中移除触发器的回调可以带来很大的收益,因此请尝试简化您的 层碰撞矩阵。您可以通过 项目设置 > 物理 编辑您的 物理设置

碰撞矩阵文档 中了解更多信息。

物理项目设置接口
修改物理项目设置以挤出更多性能。

修改模拟频率

物理引擎通过在固定时间步长上运行来工作。要查看您的项目运行的固定速率,请转到 编辑 > 项目设置 > 时间

固定时间步长 字段定义每个物理步骤使用的时间增量。例如,默认值 0.02 秒(20 毫秒)相当于 50 fps 或 50 Hz。

由于 Unity 中的每一帧所需的时间是可变的,因此它与物理模拟并不完全同步。引擎计数到下一个物理时间步长。如果一个帧运行得稍微慢一点或快一点,Unity会使用经过的时间来知道何时以适当的时间步长运行物理模拟。

如果一个帧准备时间过长,这可能会导致性能问题。例如,如果你的游戏经历了一个高峰(例如,实例化许多GameObjects或从磁盘加载文件),该帧可能需要40毫秒或更长时间才能运行。在默认的20毫秒固定时间步长下,这将导致在下一个帧上运行两个物理模拟,以便“赶上”可变时间步长。

额外的物理模拟反过来又增加了处理帧所需的时间。在低端平台上,这可能导致性能的恶性循环。

后续帧准备时间更长也使得物理模拟的积压时间更长。这导致帧变得更慢,每帧需要运行更多的模拟。结果是性能越来越差。

最终,物理更新之间的时间可能超过最大允许时间步长。在这个截止点之后,Unity开始丢弃物理更新,游戏会出现卡顿。

为了避免物理性能问题:

  • 减少模拟频率。对于低端平台,将固定时间步长增加到略高于你的目标帧率。例如,在移动设备上使用0.035秒以达到30帧每秒。这可以帮助防止性能的恶性循环。
  • 减少最大允许时间步长。使用较小的值(如0.1秒)会牺牲一些物理模拟的准确性,但也限制了每帧可以发生的物理更新数量。尝试不同的值,以找到适合你项目需求的方案。

如果必要,在帧的更新阶段选择模拟模式手动模拟物理步骤。这使您能够控制何时运行物理步骤。将 Time.deltaTime 传递给 Physics.Simulate,以保持物理与仿真时间同步。这种方法可能会导致在具有复杂物理或高度可变帧时间的场景中物理仿真不稳定,因此请谨慎使用。

Physics.Simulate 文档 中了解更多信息。

Unity 编辑器中的默认固定时间步长
项目设置中的默认固定时间步长为 0.02 秒(每秒 50 帧)。

对大型场景使用盒子修剪

Unity 物理引擎分为两个步骤:

  • 宽相位,使用 扫描和修剪 算法收集潜在碰撞
  • 窄相位,引擎实际计算碰撞

扫描和修剪宽相位的默认设置 (编辑 > 项目设置 > 物理 > 宽相位类型) 可能会为通常平坦且有许多碰撞体的世界生成误报。如果您的场景很大且大部分是平坦的,请避免此问题并切换到 自动盒子修剪多盒子修剪宽相位。这些选项将世界划分为网格,每个网格单元执行扫描和修剪。

多盒子修剪宽相位允许您手动指定世界边界和网格单元的数量,而自动盒子修剪会为您计算这些。

查看物理属性的完整列表 在这里

宽相位类型接口
物理选项中的宽相位类型

修改求解器迭代次数

如果您想更准确地模拟特定物理体,请增加其 Rigidbody.solverIterations

这会覆盖 Physics.defaultSolverIterations,您也可以在 编辑 > 项目设置 > 物理 > 默认求解器迭代 中找到。

要优化您的物理仿真,请在项目的 defaultSolveIterations 中设置相对较低的值。然后将更高的自定义 Rigidbody.solverIterations 值应用于需要更多细节的单个实例。

获取有关 Rigidbody.solverIterations 的更多信息。

默认求解器迭代
覆盖每个刚体的默认求解器迭代

禁用自动变换同步

默认情况下,Unity不会自动将变换的更改与物理引擎同步。相反,它会等待下一个物理更新,或者直到您手动调用Physics.SyncTransforms。启用此选项时,任何在该Transform或其子对象上的RigidbodyCollider会自动与物理引擎同步。

何时手动同步

当autoSyncTransforms被禁用时,Unity仅在FixedUpdate的物理仿真步骤之前或在通过Physics.Simulate显式请求时同步变换。如果您使用在变换更改和物理更新之间直接从物理引擎读取的API,您可能需要执行额外的同步。示例包括访问Rigidbody.position或执行Physics.Raycast

性能最佳实践

尽管autoSyncTransforms确保物理查询是最新的,但它会带来性能成本。每个与物理相关的API调用都会强制同步,这可能会降低性能,尤其是在多次连续查询时。遵循以下最佳实践:

  • 除非必要,否则禁用autoSyncTransforms:仅在精确、持续同步对您的游戏机制至关重要时启用它。
  • 使用手动同步:为了更好的性能,在需要最新变换数据的调用之前,手动使用Physics.SyncTransforms()同步变换。这种方法比全局启用autoSyncTransforms更有效。

了解更多关于Physics.SyncTransforms的信息。

场景中禁用自动同步
在Unity中剖析场景时禁用自动同步变换

重用碰撞回调

接触数组通常显著更快,因此一般建议使用这些而不是重用碰撞回调,但如果您确实有特定的用例,请考虑以下内容。

回调MonoBehaviour.OnCollisionEnterMonoBehaviour.OnCollisionStayMonoBehaviour.OnCollisionExit都将碰撞实例作为参数。此碰撞实例分配在托管堆上,必须进行垃圾回收。

为了减少生成的垃圾量,请启用Physics.reuseCollisionCallbacks(也可以在项目设置 > 物理 > 重用碰撞回调中找到)。启用此功能后,Unity仅为每个回调分配一个碰撞对实例。这减少了垃圾回收器的浪费并提高了性能。

一般建议始终启用重用碰撞回调以获得性能收益。您只应在代码依赖于单个碰撞类实例的遗留项目中禁用此功能,这使得存储单个字段变得不切实际。

了解更多关于Physics.reuseCollisionCallbacks的信息。

单个碰撞实例
在Unity控制台窗口中,碰撞进入和碰撞停留时有一个单个碰撞实例。

移动静态碰撞体

静态碰撞体是具有碰撞器组件但没有刚体的游戏对象。

请注意,您可以移动静态碰撞体,这与“静态”一词相反。要做到这一点,只需修改物理体的位置。累积位置变化并在物理更新之前同步。您不需要为静态碰撞体添加刚体组件以便移动它。

但是,如果您希望静态碰撞体以更复杂的方式与其他物理体交互,请为其提供运动刚体。使用Rigidbody.positionRigidbody.rotation来移动它,而不是访问变换组件。这保证了物理引擎更可预测的行为。

注意:如果需要在运行时移动或重新配置单个静态碰撞体 2D,则添加一个刚体 2D 组件并将其设置为 静态 体类型,因为当碰撞体 2D 拥有自己的刚体 2D 时,模拟速度更快。如果需要在运行时移动或重新配置一组碰撞体 2D,最好将它们全部作为单个隐藏父刚体 2D 的子对象,而不是单独移动每个游戏对象。

获取有关 刚体 的更多信息。

使用非分配查询

要在 3D 项目中检测和收集一定距离和特定方向上的碰撞体,请使用射线投射和其他物理查询,如 盒子投射。请注意,

返回多个碰撞体作为数组的物理查询,如 重叠球重叠盒,需要在托管堆上分配这些对象。这意味着垃圾收集器最终需要收集分配的对象,如果在错误的时间发生,可能会降低性能。

为了减少这种开销,请使用这些查询的 非分配 版本。例如,如果您使用重叠球来收集某个点周围的所有潜在碰撞体,请改用 重叠球非分配

这允许您传入一个碰撞体数组(结果参数)作为缓冲区。非分配方法在不生成垃圾的情况下工作。否则,它的功能与相应的分配方法相同。

请注意,在使用非分配方法时,您需要定义一个足够大小的结果缓冲区。如果缓冲区用完空间,则不会增长。

2D 物理

请注意,上述建议不适用于 2D 物理查询,因为在 Unity 的 2D 物理系统中,方法没有 "非分配" 后缀。相反,所有 2D 物理方法,包括返回多个结果的方法,都提供接受数组或列表的重载。例如,虽然 3D 物理系统有像 RaycastNonAlloc 这样的函数,但 2D 等效函数仅使用可以接受数组或 List 作为参数的 Raycast 的重载版本,如:

var results = new List();

int hitCount = Physics2D.Raycast(origin, direction, contactFilter, results);

通过使用重载,您可以在 2D 物理系统中执行非分配查询,而无需专门的非分配方法。

NonAlloc方法文档中了解更多信息。


批量查询射线检测

您可以使用Physics.Raycast运行射线查询。然而,如果您有大量的射线操作(例如,为10,000个代理计算视线),这可能会占用大量的CPU时间。

使用RaycastCommand通过C#作业系统批处理查询。这将工作从主线程卸载,以便射线可以异步并行发生。

RaycastCommands文档中查看示例。

使用物理调试器可视化

使用物理调试窗口(窗口 > 分析 > 物理调试器)帮助排除任何问题碰撞体或差异。这显示了可以相互碰撞的游戏对象的颜色编码指示器。

有关更多信息,请参见物理调试器文档

物理调试器界面
unity关键艺术21 11
更多针对Unity开发者和创作者的技巧

从Unity最佳实践中心找到更多最佳实践和技巧。从超过30个指南中选择,这些指南由行业专家、Unity工程师和技术艺术家创建,将帮助您高效地使用Unity的工具集和系统进行开发。