此页面描述了常见的性能问题以及缓解它们的最佳实践。
脚本计算
在 Luau 代码中,昂贵的操作需要更长的处理时间,因此会影响帧率。除非在并行执行,否则 Luau 代码是同步运行的,并且会阻塞主线程,直到遇到一个让线程挂起的函数。
常见问题
对表结构的密集操作 - 复杂操作如序列化、反序列化和深度克隆会产生较高的性能成本,尤其是在大型表结构上。如果这些操作是递归的或涉及迭代非常大的数据结构,这一点尤其明显。
高频率事件 - 将昂贵的操作绑定到 RunService 的基于帧的事件上,而不限制频率,意味着这些操作会在每一帧中反复执行,这通常会导致计算时间不必要地增加。这些事件包括:
缓解措施
- 在 RunService 事件中谨慎调用代码,限制使用情况,只在高频调用至关重要的情况下使用(例如,更新相机)。您可以在其他事件中或在循环中不那么频繁地执行大多数其他代码。
- 使用 task.wait() 将大型或昂贵的任务分解,以将工作分散到多个帧中。
- 识别并优化不必要的昂贵操作,对于不需要访问数据模型的计算密集型任务,使用 多线程。
- 某些服务器端脚本可以受益于 本机代码生成,一个简单的标志,可将脚本编译为机器代码而不是字节码。
MicroProfiler 范围
| 范围 | 相关计算 |
| RunService.PreRender | 在 PreRender 事件上执行的代码 |
| RunService.PreSimulation | 在 Stepped 事件上执行的代码 |
| RunService.PostSimulation | 在 Heartbeat 事件上执行的代码 |
| RunService.Heartbeat | 在 Heartbeat 事件上执行的代码 |
有关使用 MicroProfiler 调试脚本的更多信息,请参阅 debug 库,其中包括用于标记特定代码和进一步增加特异性的函数,例如 debug.profilebegin 和 debug.profileend。脚本调用的许多 Roblox API 方法也有其自身相关的 MicroProfiler 标签,这可以提供有用的信号。
脚本内存使用
当您编写消耗内存的脚本,而垃圾收集器无法在不再使用时正常释放这些内存时,内存泄漏可能会发生。泄漏在服务器上尤其普遍,因为它们可以在许多天内持续在线,而客户端会话则短得多。
在 开发者控制台 中,以下内存值可能表示需要进一步调查的问题:
- LuaHeap - 高或增长的消耗表明可能存在内存泄漏。
- InstanceCount - 实例数量持续增长表明代码中的某些实例引用未被垃圾收集。
- PlaceScriptMemory - 按脚本提供内存使用的详细信息。
常见问题
保持连接状态 - 引擎从不垃圾收集与实例连接的事件及其连接回调内部的任何值。因此,事件的活动连接和连接实例、连接函数以及引用的值在事件触发后仍被垃圾收集器认为超出了作用域。
尽管当实例被销毁时事件会断开连接,但一个常见的错误是认为这也适用于 Player 对象。在用户离开游戏后,引擎不会自动销毁他们的代表 Player 对象和角色模型,因此,如果不在您的脚本中断开连接,Player 对象及角色模型下的实例(如 CharacterAdded)仍会消耗内存。 这可能导致服务器随着数百名用户进出游戏而产生非常显著的内存泄漏。
表 - 将对象插入表中,但在不再需要时不将其移除,会导致不必要的内存消耗,特别是对于在用户加入时跟踪用户数据的表。例如,以下代码示例在每次用户加入时创建一个表以添加用户信息:
示例local playerInfo = {}Players.PlayerAdded:Connect(function(player)playerInfo[player] = {} -- 一些信息end)如果您不在不再需要时移除这些条目,表的大小会继续增长,并在更多用户加入会话时消耗更多内存。任何迭代该表的代码也会在表的大小增长时变得更为计算密集。
缓解措施
为防止内存泄漏清理所有已使用的值:
断开所有连接 - 浏览您的代码库,确保通过以下路径断开每个连接:
- 使用 Disconnect() 函数手动断开连接。
- 使用 Destroy() 函数销毁事件所属的实例。
- 销毁连接回溯到的脚本对象。
在离开后移除玩家对象和角色 - 启用 Workspace.PlayerCharacterDestroyBehavior 以在用户离开后自动销毁玩家对象和角色模型。如果您愿意,可以手动清理它们:
示例玩家和角色清理local Players = game:GetService("Players")Players.PlayerAdded:Connect(function(player)player.CharacterRemoving:Connect(function(character)task.defer(character.Destroy, character)end)end)Players.PlayerRemoving:Connect(function(player)task.defer(player.Destroy, player)end)
物理计算
过度的物理模拟可能是服务器和客户端每帧计算时间增加的主要原因。
常见问题
过度的物理时间步频率 - 默认情况下,步骤行为是 自适应模式,在该模式下,物理以 60 Hz、120 Hz 或 240 Hz 的速率进行,具体取决于物理机制的复杂性。
还可以使用提供更高精度的固定模式,该模式将所有物理组件的步频强制为 240 Hz(每帧四次)。这会导致每帧需要更多的计算。
模拟对象的复杂性数量过多 - 模拟的 3D 组件越多,物理计算每帧所需的时间就越长。通常,游戏将有不需要被模拟的对象,或者会有比需要的约束和关节更多的机制。
过于精确的碰撞检测 - 网格部件具有 CollisionFidelity 属性用于检测碰撞,提供不同性能影响水平的多种模式。网格部件的精确碰撞检测模式性能成本最高,计算时间也最长。
缓解措施
固定不需要模拟的部分 - 固定所有不需要由物理驱动的部分,例如静态 NPC。
使用自适应物理步进 - 自适应步进动态调整物理计算的速率,从而在某些情况下可以减少物理更新的频率。
减少机制复杂性
- 尽可能减少组件中的物理约束或关节数量。
- 减少机制内部的自我碰撞,例如通过施加限制或无碰撞约束来阻止布娃娃四肢彼此碰撞。
减少对网格的精确碰撞保真度的使用
对于小型或不可交互的对象,使用框体保真度,因为用户不太可能注意到差异。
对于小到中型对象,根据形状使用框体或外壳保真度。
对于大型复杂对象,尽可能使用透明部分构建自定义碰撞。
对于不需要碰撞的对象,禁用碰撞并使用框体或外壳保真度,因为碰撞几何体仍然存储在内存中。
有关如何选择平衡精度和性能要求的碰撞保真度选项的详细步骤,请参见 设置物理和渲染参数。
MicroProfiler 范围
| 范围 | 相关计算 |
| physicsStepped | 整体物理计算 |
| worldStep | 每帧进行的离散物理步骤 |
物理内存使用
物理运动和碰撞检测会消耗内存。网格部件具有 CollisionFidelity 属性,用于评估网格的碰撞边界。
常见问题
默认和精确的碰撞检测模式消耗的内存明显高于两个其他模式,它们的碰撞形状保真度较低。
如果您在 PhysicsParts 下看到高内存消耗,则可能需要探索减少游戏中对象的 碰撞保真度。
如何缓解
要减少碰撞保真度使用的内存:
- 使用 CollisionFidelity 设置降低碰撞保真度。Box 的内存开销最低,而 Default 和 Precise 通常较高。
- 通常,将任何小型固定部件的碰撞保真度设置为 Box 是安全的。
- 对于非常复杂的大网格,您可能希望使用小物体构建具有框体碰撞保真度的自定义碰撞网格。
人形
Humanoid 是一个类,提供了广泛的功能,适用于玩家和非玩家角色(NPC)。虽然功能强大,但是 Humanoid 会带来显著的计算成本。
常见问题
- 在 NPC 上启用所有 HumanoidStateTypes - 保持某些 HumanoidStateTypes 启用会带来性能成本。禁用 NPC 不需要的任何状态。例如,除非您的 NPC 会爬梯子,否则可以安全地禁用 Climbing 状态。
- 在不必要的情况下使用人形 - 不移动的静态 NPC 通常不需要 Humanoid 类。
- 从服务器播放大量 NPC 动画 - 服务器运行的 NPC 动画需要在服务器上进行模拟,并复制到客户端。这会产生不必要的开销。
- 进行不必要的大小和比例变化 - 大小/比例变化会导致 FastCluster 重新构建。如果您看到与 FastCluster 相关的性能问题,请尽量在游戏过程中减少这些变化。同样,其他属性变化也可能导致 FastCluster 被重建,因此一般来说,尽可能减少这些变化。
缓解措施
- 在客户端播放 NPC 动画 - 在有大量 NPC 的游戏中,考虑在客户端创建 Animator 并在本地运行动画。这减少了服务器的负担,并降低了不必要的复制需求。同时也使得更多的优化成为可能(例如,仅为靠近角色的 NPC 播放动画)。
- 使用性能友好的人形替代品 - NPC 模型不一定需要包含人形对象。
- 对于静态 NPC,使用简单的 AnimationController,因为它们不需要移动,只需要播放动画。
- 对于移动中的 NPC,考虑实现自己的移动控制器,并根据 NPC 的复杂性使用 AnimationController 来处理动画。
- 禁用未使用的人形状态 - 使用 Humanoid:SetStateEnabled() 仅启用每个人形所需的状态。
- 为频繁重生的 NPC 模型进行对象池 - 不要完全销毁 NPC,而是将 NPC 发送到一个非活动 NPC 池中。这样,当需要重生新的 NPC 时,您只需重新激活池中的一个 NPC。此过程称为对象池,可以最小化需要实例化角色的次数。
- 仅在用户附近生成 NPC - 当用户不在范围内时,不要生成 NPC,当用户离开范围时,剪裁它们。
- 避免在实例化后对角色层次结构进行更改 - 对角色层次结构的某些修改会对性能产生重大影响。一些优化是可以用的:
- 如果您需要将任何 BasePart 对象附加到角色,请在角色模型的层次结构之外进行。
MicroProfiler 范围
| 范围 | 相关计算 |
| stepHumanoid | 人形控制和物理 |
| stepAnimation | 人形和动画师动画 |
| updateInvalidatedFastClusters | 与实例化或修改角色相关 |
渲染
客户端每帧消耗的大部分时间都用于渲染当前帧的场景。服务器不进行任何渲染,因此此部分仅限于客户端。
绘制调用
绘制调用是引擎对 GPU 的一组指令,以渲染某些内容。绘制调用有显著的开销。通常,每帧的绘制调用越少,渲染一帧花费的计算时间就越少。
您可以通过 Studio 中的 Render Stats ⟩ Timing 项查看当前发生的绘制调用数量。您可以通过按 ShiftF2 在客户端查看 Render Stats。
在给定帧中,需要绘制到场景中的对象越多,就会向 GPU 发送更多的绘制调用。然而,Roblox 引擎利用一种称为 实例化 的过程,将具有相同纹理特征的相同网格折叠为一次绘制调用。具体而言,当以下条件满足时,具有相同 MeshContent 的多个网格会在一次绘制调用中处理:
- 如果存在,SurfaceAppearances 相同,否则当 TextureContents 相同时。
- 当 SurfaceAppearance 和 MeshPart.TextureID 都不存在时,材料也相同。
其他常见问题
过度的对象密度 - 如果大量对象高度集中,则渲染该场景区域需要更多的绘制调用。如果您发现查看地图的某个部分时帧率下降,这可能是该区域对象密度过高的良好信号。
对于像贴图、纹理和粒子这样的对象,其处理不是批处理的,会引入额外的绘制调用。在场景中对这些对象类型特别注意。尤其是对 ParticleEmitters 的属性更改可能对性能产生显著影响。
错失实例化机会 - 通常,场景会包含多次复制的相同网格,但是每个复制的网格都具有不同的网格或纹理资产 ID。这会阻止实例化,并导致不必要的绘制调用。
此问题的一个常见原因是在一次导入整个场景,而不是将单个资产导入到 Roblox 中,然后在导入后复制以组装场景。
即使是像下面这样简单的脚本,也可以帮助您识别使用不同网格 ID 的相同名称的网格部件:
for _,descendant in workspace:GetDescendants() doif descendant:IsA("MeshPart") thenprint(descendant.Name .. ", " .. descendant.MeshId)endend输出(启用 Stack Lines)可能如下所示。重复的行表示相同网格的重用,这是好的。独特的行不一定是坏事,但根据您的命名方案,可能表明游戏中存在重复的网格:
LargeRock, rbxassetid://106420009602747 (x144) -- goodLargeRock, rbxassetid://120109824668127LargeRock, rbxassetid://134460273008628LargeRock, rbxassetid://139288987285823LargeRock, rbxassetid://71302144984955LargeRock, rbxassetid://90621205713698LargeRock, rbxassetid://113160939160788LargeRock, rbxassetid://135944592365226 -- all possible duplicates过度的对象复杂性 - 尽管绘制调用的数量并不像绘制调用的数量那么重要,但场景中的三角形数量确实会影响渲染一帧所需的时间。具有非常大量非常复杂网格的场景是一个常见问题,多数处于 MeshPart.RenderFidelity 属性设置为过多的 Precise 的场景也是。
过度的阴影投射 - 处理阴影是一个昂贵的过程,包含大量小部件并投射阴影的光对象的地图(或受阴影影响的小部件)可能会出现性能问题。
高透明度过度绘制 - 将部分透明的对象放在彼此附近会强制引擎多次渲染重叠像素,从而可能损害性能。有关识别和修复此问题的更多信息,请参见 删除分层透明度。
不必要的已绑定 MeshPart 移动 - 属于没有人形的模型的已绑定 MeshParts 使用空间组织的 FastClusters 分组。当这些 MeshParts 移动时,它们必须不断添加到这些空间簇中并从中删除,从而迫使簇重新构建并影响性能。
- 一个非常有效的解决方法是在模型中嵌入一个人形。人形的存在会覆盖默认的空间聚类行为,强制整个模型使用单个统一的 FastCluster。因此,位置更新不再需要聚类重建,从而缓解性能瓶颈。应仅为预期移动的 MeshParts 保留此技术,因为它可能会引入内存开销并消除空间优化的好处。我们建议在进行这些类型的更改后始终为您的游戏进行分析。有关更多信息,请参见 人形性能提示。
在 Model 中的部件过多 - 模型中部件过多可能导致重建的频率变得更高,因为部件的属性变更需要全面重建。在使用 FastCluster 时,找到模型中部件数量的良好平衡。
缓解措施
实例化相同网格并减少唯一网格的数量 - 如果您确保所有相同的网格具有相同的底层资产 ID,引擎就可以识别并在一次绘制调用中渲染它们。确保仅在地图中上传每个网格一次,然后在 Studio 中复制它们以供重用,而不是整体导入大型地图,这可能导致相同的网格具有不同的内容 ID,并被引擎识别为独特资产。包 是一种用于对象重用的有用机制。
剪裁 - 剪裁描述了消除不影响最终渲染帧的对象的绘制调用的过程。默认情况下,引擎跳过位于相机视野外的对象的绘制调用(体积裁剪),以及由其他对象遮挡视图的部件、网格和地形(遮挡裁剪)。在某些情况下,例如室内环境,您可能能够实现房间或门户系统,手动剪裁对象以进一步减少绘制调用或整体计算负载。
减少模型的细节等级 - 启用 实例流式传输 并将世界模型的 LevelOfDetail 属性设置为 SLIM,以在离相机的距离增加时渲染 优化过的轻量级 SLIM 网格。
减少角色的细节等级 - 启用实例流式传输,并将 Workspace.EnableSLIMAvatars 属性设置为,在离相机的距离增加时渲染平台角色模型,添加任意数量的配件或服装层,作为 优化过的轻量级角色表示,完全支持动画。
降低渲染保真度 - 将 MeshPart.RenderFidelity 设置为 Automatic 或 Performance。这允许网格退回到较不复杂的替代方案,从而减少需要绘制的多边形数量。
在合适的部分和光对象上禁用阴影投射 - Roblox 引擎会根据客户端图形质量级别自动降低阴影质量低于 4 的时候,最终完全禁用阴影。然而,您可以选择性地在光对象和部件上禁用阴影投射属性,以在启用阴影的同时增强性能,并增加阴影保持启用的可能性。您可以在编辑时间或动态运行时进行的优化示例包括:
使用 BasePart.CastShadow 属性禁用小部件的阴影投射,尤其是在用户相机远离时。这种策略在应用于用户相机远离的部分时尤其有效。
在可能的情况下禁用移动对象的阴影。
在不需要投射阴影的光实例上禁用 Light.Shadows。
限制光实例的范围和角度。
使用更少的光实例。
考虑在室内环境中根据特定范围或逐房间禁用光。
MicroProfiler 范围
| 范围 | 相关计算 |
| Prepare and Perform | 整体渲染 |
| Perform/Scene/computeLightingPerform | 光网格和阴影更新 |
| LightGridCPU | 体素光网格更新 |
| ShadowMapSystem | 阴影映射 |
| Perform/Scene/UpdateView | 渲染和粒子更新的准备 |
| Perform/Scene/RenderView | 渲染和后期处理 |
网络和复制
网络和复制描述了在服务器和连接客户端之间发送数据的过程。每帧在客户端和服务器之间发送信息,但大量信息需要更多的计算时间。
常见问题
过度的远程流量 - 通过 RemoteEvent 或 RemoteFunction 对象发送大量数据或非常频繁地激活它们,可能导致每帧处理传入数据包消耗大量 CPU 时间。常见错误包括:
- 每帧复制不需要的重复数据。
- 在用户输入时进行数据复制,而没有任何节流机制。
- 分发超过所需的数据。例如,当玩家购买一个物品时发送整个库存,而不是仅发送购买的项目的详细信息。
复杂实例树的创建或删除 - 当对服务器上的数据模型进行更改时,会复制到连接的客户端。这意味着在运行时创建和销毁大型实例层次结构,例如地图,可能会对网络造成很大的压力。
此问题的常见原因是 动画编辑器 插件在模型中保存的复杂动画数据。如果在游戏发布之前未将这些数据删除,且动画模型被频繁克隆,将会复制大量不必要的数据。
服务器端 TweenService - 如果 TweenService 用于服务器端 tween 一个对象,则每帧都会将 tweened 属性复制到每个客户端。这不仅会导致客户端延迟波动时 tween 不稳定,还会导致大量不必要的网络流量。
缓解措施
您可以采取以下策略来减少不必要的复制:
- 避免通过远程事件一次发送大量数据。相反,仅在较低频率下发送必要数据。例如,对于角色状态,当其更改时进行复制,而不是每帧。
- 将复杂的实例树分块,例如地图,并逐块加载它们,以分散这些复制的工作。
- 清理动画元数据,尤其是模型的动画目录,在导入后。
- 限制不必要的实例复制,尤其是在服务器不需要了解创建的实例的情况下。这包括:
- 视觉效果,例如爆炸或魔法弹射。服务器只需要知道位置,以确定结果,而客户端可以在本地创建视觉效果。
- 第一人称物品视图模型。
- 在客户端而不是在服务器上 tween 对象。
MicroProfiler 范围
| 范围 | 相关计算 |
| ProcessPackets | 处理传入网络数据包的操作,例如事件调用和属性更改 |
| Allocate Bandwidth and Run Senders | 与服务器相关的输出事件 |
资产内存使用
提高客户端内存使用效率的最大影响机制是启用 实例流式传输。
实例流式传输
实例流式传输选择性地加载不需要的数据模型部分,这可以显著减少加载时间,并提高客户端在内存压力下防止崩溃的能力。
如果您遇到内存问题且已禁用实例流式传输,请考虑更新您的游戏以支持它,特别是当您的 3D 世界很大时。实例流式传输是基于 3D 空间中的距离,因此较大的世界自然受益更多。
如果启用实例流式传输,您可以提高其攻击性。例如,考虑:
- 尽可能减少对 Enum.ModelStreamingMode.Persistent 的使用。如果您使用它作为兼容性措施,则可能需要更新您的脚本。
- 减少 Workspace.StreamingMinRadius 和 Workspace.StreamingTargetRadius。
有关流式传输选项及其好处的更多信息,请参见 流式传输属性。
其他常见问题
资产重复 - 一个常见错误是多次上传相同资产,导致不同资产 ID。这可能导致相同内容被多次加载到内存中。
资产体积过大 - 即使资产不相同,也有可能错过重用相同资产并节省内存的机会。
音频文件 - 音频文件可能是内存使用的意外贡献者,特别是在您一次将它们全部加载到客户端时,而不是仅在游戏的一部分中加载所需的内容。有关策略,请参见 加载时间。
高分辨率纹理 - 纹理的图形内存消耗与纹理在磁盘上的大小无关;纹理中的像素数量决定了内存使用。例如,1024x1024 像素的纹理消耗的图形内存是 512x512 纹理的四倍。
上传到 Roblox 的图像被转码为固定格式,因此在与每像素字节数较少的颜色模型相关联时上传图像并没有内存优势。同样,在上传之前压缩图像或移除不需要的图像的 alpha 通道可能会减少磁盘上的图像大小,但不会改善内存使用。
当游戏加载时,引擎会自动以较低质量的纹理开始,然后根据可用设备内存、与相机的距离、纹理占用的屏幕空间量以及其他因素提高质量。尽管如此,有策略地调整纹理大小可以改善您的游戏的内存使用。
缓解措施
只上传资产一次 - 在对象之间重用相同的资产 ID,确保相同的资产(尤其是网格和图像)未被多次分开上传。
查找并修复重复资产 - 查找上传多次且具有不同 ID 的相同网格部件和纹理。
- 尽管没有 API 可以自动检测资产的相似性,您可以收集所有在您场所中的图像资产 ID(手动或使用脚本),下载它们并使用外部比较工具进行比较。
- 对于网格部件,最佳策略是采用独特的网格 ID,并按大小对其进行分类,以手动识别重复项。
- 不要为不同颜色使用不同的纹理,而是上传单个纹理,并使用 SurfaceAppearance.Color 属性为其应用不同的色调。
单独导入地图资产 - 不要一次导入整个地图,而是单独导入和重建地图中的资产。导入器不会对网格进行去重,因此如果您导入一个大型地图并且那里有很多单独的地板瓷砖,每个瓷砖都将作为单独资产导入(即使它们是重复的)。这可能会导致后续的性能和内存问题,因为每个网格被单独处理,并占用内存和绘制调用。
将图像的像素限制在不超过必要数量。除非图像在屏幕上占用很大的物理空间,否则通常最多需要 512x512 像素。大多数次要图像应小于 256x256 像素。
使用修整贴图 确保 3D 地图中的最大纹理重用。有关如何创建修整贴图的步骤和示例,请参见 创建修整贴图。
您可能还想考虑使用精灵图表将许多较小的 UI 图像作为单个图像加载。然后,您可以使用 ImageLabel.ImageRectOffset 和 ImageLabel.ImageRectSize 来显示图表的一部分。
加载时间
许多游戏实现自定义加载屏幕,并使用 ContentProvider:PreloadAsync() 方法请求资产,以便在后台下载图像、声音和网格。
这种方法的优点是可以确保游戏的重要部分在无 pop-in 的情况下完全加载。但是,常见的错误是过度利用此方法预加载比实际需要更多的资产。
一个坏的做法示例是加载整个 Workspace。虽然这可能防止纹理 pop-in,但它显著增加了加载时间。
另一个类似的做法是使用 ContentProvider.RequestQueueSize 来确保所有请求的资产已完成加载。然而,这会带来同样显著增加加载时间的问题,同时也是一种由于起伏性质而不可靠的方法。
相反,仅在必要情况下使用 ContentProvider:PreloadAsync(),包括:
- 加载屏幕中的图像。
- 游戏菜单中重要的图像,如按钮背景和图标。
- 起始或生成区域中重要的资产。
如果您必须加载大量资产,建议提供 跳过加载 按钮。