本页描述了常见的性能问题和应对它们的最佳实践。
脚本计算
在 Luau 代码中的昂贵操作需要更长的处理时间,因此可能会影响帧率。除非它并行执行,否则 Luau 代码会同步运行,并阻塞主线程直到遇到一个函数,该函数返回线程。
常见问题
对表结构的密集操作 - 复杂操作,例如序列化、反序列化和深度克隆,在大型表结构上会带来高成本,特别是在大型表结构上。这尤其适用于如果这些操作是递归的或涉及对非常大的数据结构的迭代。
高频事件 - 将昂贵的操作捆绑到框架基于事件的 RunService 中,而不限制频率,这些操作每个框架都会重复,这往往会导致计算时间的不必要增加。这些事件包括:
缓解
- 在 RunService 事件上调用代码时,稀有地限制使用情况,例如高频率调用是必要的(例如,更新相机)。您可以在其他事件中执行大多数其他代码,或在循环中较少频繁地执行。
- 使用 task.wait() 分解大或昂贵的任务,将工作分布到多个框架上。
- 识别并优化不必要昂贵的操作,并使用 多线程 来执行那些不需要访问数据模型的计算成本高的任务。
- 某些服务器端脚本可以受益于 本地代码生成,一种简单的旗帜,可以将脚本编译为机器代码而不是字节代码。
微型调试器范围
范围 | 相关计算 |
运行服务.PreRender | 在预渲染事件上执行代码 |
运行服务.PreSimulation | 在步骤事件上执行代码 |
运行服务.PostSimulation | 在心跳事件上执行代码 |
运行服务.心跳 | 在心跳事件上执行代码 |
有关使用微型调试器调试脚本的更多信息,请参阅 debug 图书馆,其包含标记特定代码和进一步提高特定性的函数,例如 debug.profilebegin 和 debug.profileend 。由脚本调用的许多 Roblox API 方法也有自己的关联微调器标签,可以提供有用的信号。
脚本内存使用
当您写脚本时,可能会发生内存泄漏,因为垃圾收集器在不再使用时无法正确释放内存。泄漏在服务器上特别普遍,因为它们可以连续多天在线,而客户端会话要短得多。
在 开发者控制台 中的以下内存值可能表示需要进一步调查的问题:
- Lua堆 - 高或增长的消耗建议存在内存泄漏。
- 实例数量 - 一致增长的实例数量表明您的代码中的某些实例没有被垃圾收集。
- 放置脚本记忆 - 提供通过脚本分解内存使用的脚本。
常见问题
表格 - 将对象插入表,但当不再需要时未删除它们会导致不必要的内存消耗,尤其是当它们加入时跟踪用户数据的表。例如,以下代码示例每次用户加入时创建一个添加用户信息的表:
例子local playerInfo = {}Players.PlayerAdded:Connect(function(player)playerInfo[player] = {} -- 一些信息end)如果您不在这些入口不再需要时移除它们,表的大小将继续增长,并且随着更多用户加入会话,消耗更多内存。随着表的尺寸增加,对这个表进行循环的任何代码也会变得更加计算成本高昂,因为表的尺寸会增加。
缓解
要清理所有使用的值以防止泄漏内存:
断开所有连接 - 通过以下路径之一确保每个连接都被清理干净:
- 使用 Disconnect() 函数手动断开。
- 使用 Destroy() 函数摧毁属于该事件的实例。
- 摧毁连接追溯到的脚本对象。
在离开后移除玩家对象和角色 - 实现代码以确保用户离开后仍然没有连接,如以下示例所示:
例子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。
使用适应物理的步骤 - 适应步骤动态调整物理机制的计算速率,在某些情况下减少物理更新的频率。
减少机制复杂性 * 尽可能减少装配中的物理约束或接口数量。
- 减少机制内的自我碰撞量,例如通过对布娃娃肢应用限制或无碰撞约束来防止它们相互碰撞。
减少网格精确碰撞精度的使用 * 对于用户很少注意到差异的小或不可交互的对象,使用盒子忠实度。
对于小型到中型对象,使用箱或船体的忠实度,取决于形状。
对于大而复杂的对象,尽可能使用隐形部件来构建自定义碰撞。
对于不需要碰撞的对象,禁用碰撞并使用箱子或船体的忠实度,因为碰撞几何仍然存储在内存中。
要了解有关如何选择一个平衡精度和性能要求的碰撞保真选项的深入教程,请参阅设置物理和渲染参数。
微型调试器范围
范围 | 相关计算 |
物理步骤 | 整体物理计算 |
世界步 | 每帧采取的分散物理步骤 |
物理内存使用
物理移动和碰撞检测消耗内存。网格部件有一个 CollisionFidelity 属性,决定用于评估网格碰撞边界的方法。
常见问题
默认和精确的碰撞检测模式消耗的内存比其他两种模式(精度较低的碰撞形状)要大得多。
如果你在 物理部件 下看到高的内存消耗水平,你可能需要探索减少体验中对象的 碰撞精度。
如何缓解
为了减少用于碰撞一致性的内存:
- 使用 CollisionFidelity 设置减少碰撞的忠实度。Box 拥有最低的内存开销,而 Default 和 Precise 一般来说更昂贵。
- 一般来说,设置任何小型锚定部件的碰撞精度为 Box 是安全的。
- 对于非常复杂的大型网格,您可能想使用箱子碰撞精度较小的对象来构建自己的碰撞网格。
人形怪物
Humanoid 是一个提供广泛功能给玩家和非玩家角色(NPC)的类。虽然强大,一个 Humanoid 带有重大的计算成本。
常见问题
- 将所有人形状态类型在 NPC 上启用 - 启用某些 HumanoidStateTypes 会有性能成本。禁用任何不需要为您的 NPC 使用的东西。例如,除非你的 NPC 要攀爬梯子,否则禁用Climbing是安全的。
- 经常使用机器人模型来实现、修改和重生模型 * 这可能需要引擎处理,尤其是如果这些模型使用 分层服装 。这也可能在经常重生的虚拟形象体验中特别具有问题。
- 在 微型调试器 中,长时间的 更新不支持快速集群 标签(超过 4 ms)经常是虚拟形象即时化/修改触发过多无效化的信号。
- 在不需要的情况下使用人形物在 - 不移动的静态 NPC 一般没有需要 Humanoid 类。
- 从服务器播放动画到大量 NPC - 在服务器上运行的 NPC 动画需要在服务器上模拟并复制到客户端。这可能是不必要的开销。
缓解
- 在客户端播放 NPC 动画 - 在具有大量 NPC 的体验中,考虑在客户端上创建 Animator 并本地运行动画。这减少了服务器的负载和不必要的复制需求。它还可以实现额外的优化(例如只为靠近角色的 NPC 播放动画)。
- 使用性能友好的替代方案来替换 Humanoids - NPC 模型不一定需要包含一个人形对象。
- 对于静态 NPC,使用简单的 AnimationController ,因为它们不需要移动,只需要播放动画。
- 对于移动 NPC,考虑实现自己的移动控制器并使用 AnimationController 来执行动画,根据 NPC 的复杂程度进行。
- 禁用未使用的人形状态 - 使用 Humanoid:SetStateEnabled() 仅启用每个人形状态所需的状态。
- 频繁重生的泳池 NPC 模型 - 而不是完全摧毁一个 NPC,将 NPC 发送到一组不活跃的 NPC 中。这样,当需要重生新 NPC 时,你可以简单地重新激活泳池中的一个 NPC。该过程称为泳池化,可最大限度地减少字符需要被实例化的次数。
- 仅在用户靠近时生成 NPC,否则不要生成 NPC,当用户离开范围时收集它们 - 不要在用户不在范围内时生成 NPC,当用户离开范围时收集它们
- 避免在实例化后对虚拟形象层次结构进行更改 - 对虚拟形象层次结构的某些修改会对性能产生重大影响。有一些优化可用:
- 如果需要将任何 BasePart 对象附加到虚拟形象上,请在虚拟形象的等级之外进行 Model 。
微型调试器范围
范围 | 相关计算 |
步行人形态 | 人形控制和物理学 |
步骤动画 | 人形和动画设计师动画 |
更新无效的快速集群 | 与创建或修改虚拟形象相关 |
渲染
客户端花在每个框架上的时间的重要部分是在当前框架中渲染场景。服务器不进行任何渲染,因此此部分仅供客户端使用。
发出绘制调用
一个绘制调用是从引擎到 GPU 的一组指令,用于渲染某些内容。绘制调用有显著的开销。一般来说,每帧的调用次数越少,渲染框架的计算时间就越少。
您可以在 Studio 中查看多少绘制调用正在发生,以及与 渲染统计 > 时间 项目的关系。您可以通过按 渲染统计 在客户端按Shift查看F2。
在特定帧中需要在场景中绘制的对象越多,对 GPU 的绘制调用就越多。然而,Roblox 引擎使用名为 实例化 的过程将具有相同纹理特性的相同网格压缩到单个绘制调用。具体来说,具有相同 MeshId 的多个网格在单次绘制调用中处理时:
- 当 both SurfaceAppearance 和 MeshPart.TextureID 不存在时,材料是相同的。
其他常见问题
过度的对象密度 - 如果大量对象聚集在高密度,那么渲染场景的这一区域需要更多的绘制调用。如果您在查看地图的某部分时发现帧率下降,这可能是一个很好的信号,表明该区域的对象密度过高。
像图标、纹理和粒子等对象不能批量处理,并且会导致额外的绘制调用。在场景中给予这些对象类型额外关注。特别是,属性更改到 ParticleEmitters 可能会对性能产生巨大影响。
错过了实例化机会 - 经常情况下,场景会包含同一网格重复多次,但每个网格复制的网格或纹理资产ID各不相同。这会导致实例化失败,可能会导致不必要的绘制调用。
这个问题的一个常见原因是,当整个场景一次导入时,而不是单个资产被导入到 Roblox 然后在导入后复制以组装场景。
即使是像这样简单的脚本也可以帮助你识别使用不同网格 ID 的具有相同名称的网格部件:
local Workspace = game:GetService("Workspace")for _, descendant in Workspace:GetDescendants() doif descendant:IsA("MeshPart") thenprint(descendant.Name .. ", " .. descendant.MeshId)endend输出(启用了 堆栈线 )可能会看起来像这样。重复的线表示使用相同的网格,这很好。独特的线并不一定是坏的,但取决于你的命名方案,可能会在你的体验中显示重复的网格:
LargeRock, rbxassetid://106420009602747 (x144) -- goodLargeRock, rbxassetid://120109824668127LargeRock, rbxassetid://134460273008628LargeRock, rbxassetid://139288987285823LargeRock, rbxassetid://71302144984955LargeRock, rbxassetid://90621205713698LargeRock, rbxassetid://113160939160788LargeRock, rbxassetid://135944592365226 -- all possible duplicates过度复杂的对象 - 虽然不像调用数量那样重要,但场景中的三角形数量会影响渲染框架所需的时间。拥有非常大数量的非常复杂网格的场景是一个常见问题,场景拥有 MeshPart.RenderFidelity 属性设置为 Enum.RenderFidelity.Precise 对太多网格时也是如此。
过度投射阴影 - 处理阴影是一项昂贵的过程,包含大量投射阴影的地图(或大量受阴影影响的小部件)可能会出现性能问题。
高透明度溢出 - 将部分透明度的对象放置在一起,强迫引擎多次渲染重叠像素,可能会影响性能。了解有关识别和解决此问题的更多信息,请参阅删除分层透明度。
缓解
- 实例化相同的网格并减少独特网格数量 - 如果您确保所有相同网格都具有相同的基础资产 ID,引擎可以识别并在单次绘制调用中渲染它们。请确保只在地图上一次上传每个网格,然后在工作室复制它们,而不是导入大型地图整体,这可能导致相同的网格拥有不同的内容 ID 并被引擎识别为独特资产。包是对象重用的有用机制。
- 减少渲染精度 - 将渲染精度设置为 自动 或 性能 。这允许网格回到较少复杂的替代方案,从而减少需要绘制的多边形数量。
- 禁用适当部件和灯光对象上的阴影投射功能 - 通过选择性禁用阴影投射属性对灯光对象和部件上的阴影投射功能,场景中的阴影复杂度可以减少。这可以在编辑时或在运行时动态完成。一些例子是:
使用 BasePart.CastShadow 属性来禁用在小部件上投射阴影的可能性,因为阴影很可能不会被发现。这特别有效,当仅应用于远离用户相机的部件时。
如果可能,禁用移动对象上的阴影。
关闭Light.Shadows在轻型实例上,对象不需要投射阴影的情况下。
限制光源实例的范围和角度。
使用更少的轻型实例。
考虑禁用特定范围外或按房间分开的灯光,以便在室内环境中。
微型调试器范围
范围 | 相关计算 |
准备和执行 | 整体渲染 |
执行/场景/计算光源执行 | 轻型网格和阴影更新 |
轻型网格CPU | 声量光网更新 |
阴影地图系统 | 阴影映射 |
执行/场景/更新视图 | 准备渲染和粒子更新 |
执行/场景/渲染视图 | 渲染和后期处理 |
网络和复制
网络和复制描述了数据在服务器和连接的客户端之间发送的过程。信息每帧之间发送给客户端和服务器,但更大的信息需要更多的计算时间。
常见问题
过度的远程交通 - 通过 RemoteEvent 或 RemoteFunction 对象发送大量数据或频繁调用它们可能会导致每帧收到的包的处理时间大量消耗。常见错误包括:
- 复制每个不需要复制的框架的数据。
- 没有任何机制限制用户输入的数据复制。
- 发送超过需要的数据。例如,发送玩家购买物品时整个库存而不仅仅是购买的物品细节。
创建或移除复杂实例树 - 当服务器上的数据模型发生更改时,它会复制到连接的客户端。这意味着创建和摧毁在运行时创建和摧毁像地图一样的大规模实例架构可能需要非常多的网络资源。
一个常见的罪魁是动画编辑器插件在骨架中保存的复杂动画数据。如果在发布游戏之前这些内容未被删除,并且动画模型定期复制,将会复制大量不必要的数据。
服务器端渐进服务 - 如果 TweenService 用于渐进对象服务端,渐进的属性每帧都会复制到每个客户端。这不仅导致青少年在客户端延迟波动时紧张,还会导致大量不必要的网络流量。
缓解
您可以采用以下策略来减少不必要的复制:
- 避免一次通过远程事件发送大量数据 。相反,只在较低频率发送必要数据。例如,对于角色的状态,复制它每次更改而不是每帧。
- 拆分复杂的实例树 像地图一样,将它们分块加载以分配这些在多个框架上复制的工作。
- 清理动画元数据 ,特别是导入后的动画目录的骨架。
- 限制不必要的实例复制 ,尤其是在服务器不需要知道创建的实例的情况下。这包括:
- 视觉效果,例如爆炸或魔法攻击波。服务器只需要知道位置来确定结果,而客户端可以在本地创建视觉效果。
- 第一人称物品视图模型。
- 在客户端而不是服务器上预览对象。
微型调试器范围
范围 | 相关计算 |
处理包 | 处理来自网络的包裹,例如事件调用和属性更改 |
分配带宽并运行发送者 | 服务器上相关的输出事件 |
资产内存使用
创建者可以使用的最高影响机制来改善客户端内存使用是启用实例传播。
实例流式传输
实例流选择性加载不需要的数据模型部分,可以大大减少加载时间并提高客户端在面临内存压力时防止崩溃的能力。
如果您遇到内存问题且已禁用实例流式传输,请考虑升级您的体验以支持它,尤其是如果您的 3D 世界很大。实例传播基于 3D 空间的距离,因此更大的世界自然更受益。
如果实例传播启用,你可以提高它的攻击性。例如,考虑:
- 减少使用持久的 流传输完整性 。
- 减少 传输半径 。
了解有关流媒体选项和其优势的更多信息,请参阅流媒体属性。
其他常见问题
- 资产复制 - 一个常见的错误是多次上传相同的资产,导致不同的资产ID。这可能导致同一内容多次加载到内存。
- 过大的资产量 - 即使资产不相同,也会有情况错过重复使用相同资产并节省内存的机会。
- 音频文件 - 音频文件可能是一个令人惊讶的内存使用贡献者,特别是如果你一次加载所有它们而不仅加载你需要的一部分体验的内容。有关策略,请参阅加载时间。
- 高分辨率纹理 - 图形内存消耗对纹理的大小无关,而是与纹理中的像素数有关。
- 例如,1024x1024像素纹理消耗的图形内存是 512x512纹理的四倍。
- 上传到 Roblox 的图像会被转换为固定格式,因此没有通过将图像上传到颜色模型与每像素相关的较少字节的优惠来上传图像的内存优势。同样,在上传或从不需要它的图像中删除 Alpha 通道之前压缩图像,可以减少图像在磁盘上的大小,但是没有改善或只有很少地改善内存使用。虽然引擎会自动在一些设备上降低纹理分辨率,但降级的程度取决于设备特性,过高的纹理分辨率仍然可能导致问题。
- 您可以通过在 开发者控制台 扩展 图形图像类别 来识别给定纹理的图形内存消耗。
缓解
只上传资产一次 - 重复使用相同的资产ID,确保相同的资产,特别是网格和图像,不会多次单独上传。
查找并修复重复资产 - 寻找具有相同网格部件和纹理,且多次上传到不同ID的资产。
- 虽然没有 API 自动检测资产的相似性,但您可以在地方收集所有图像资产 ID(通过手动操作或脚本进行),下载它们,并使用外部比较工具进行比较。
- 对于网格部件,最好的策略是采取独特的网格 ID 并按尺寸组织它们,手动识别重复项。
- 而不是使用不同颜色的独立纹理,上传单个纹理,然后使用 SurfaceAppearance.Color 属性将各种染色应用到它上。
单独导入地图中的资产 - 而不是一次导入整个地图,单独导入地图中的资产并重建它们。3D导入器不会对网格进行重复,因此如果你导入了一个大型地图,包含许多单独的地板砖,那么每块地板砖都会导入为单独的资产(即使它们是重复的)。这可能会导致线程中的性能和内存问题,因为每个网格都被视为独立并占用内存和绘制调用。
限制图像像素不超过必要数量 到不超过必要数量。除非图像占用了屏幕上大量的物理空间,否则通常需要最多 512x512 像素。大多数小图像应小于 256x256 像素。
使用修边表格 以确保在 3D 地图中最大限度地重复纹理。有关如何创建修边表的步骤和例子,请参阅创建修边表。
您还可以考虑使用像素表来加载许多更小的用户界面图像作为单个图像。然后,你可以使用 ImageLabel.ImageRectOffset 和 ImageLabel.ImageRectSize 来显示表单的部分。
加载时间
许多体验实现了自定义加载屏幕,使用 ContentProvider:PreloadAsync() 方法请求资产,以便图像、声音和网格在背景下下载。
该方法的优势是,它可以让你确保体验的重要部分无需弹出即可完全加载。然而,一个常见的错误是过度使用此方法来预加载超过实际需要的更多资产。
一个恶劣实践的例子是加载整个Workspace。虽然这可能会阻止纹理弹出,但它显著增加了加载时间。
相反,只在必要的情况下使用 ContentProvider:PreloadAsync(),这包括:
- 加载屏幕上的图像。
- 体验菜单中的重要图像,例如按钮背景和图标。
- 启动或生成区域中的重要资产。
如果您必须加载大量资产,我们建议您提供 跳过加载 按钮。