改进性能

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

本页描述了常见的性能问题和应对它们的最佳实践。

脚本计算

在 Luau 代码中的昂贵操作需要更长的处理时间,因此可能会影响帧率。除非它并行执行,否则 Luau 代码会同步运行,并阻塞主线程直到遇到一个函数,该函数返回线程。

常见问题

缓解

  • RunService 事件上调用代码稀有地,限制使用情况仅限于那些需要高频率调用的情况(例如,更新相机)。您可以在其他事件中执行大多数其他代码,或在循环中更少频繁地执行。
  • 使用 task.wait() 分解大或昂贵的任务,将工作分布到多个框架上。
  • 识别并优化不必要昂贵的操作,并使用 多线程 来执行那些不需要访问数据模型的计算成本高的任务
  • 某些服务器端脚本可以受益于 本地代码生成,一种简单的旗帜,可以将脚本编译为机器代码而不是字节代码。

微型调试器范围

涉及范围关联计算
运行服务.预渲染在预渲染事件上执行代码
运行服务.预模拟在步骤事件上执行代码
RunService.PostSimulation在心跳事件上执行代码
RunService.心跳在心跳事件上执行代码

有关使用微调试器调试脚本的更多信息,请参阅 debug 图书馆,其包含标记特定代码和进一步提高特定性的函数,例如 debug.profilebegindebug.profileend 。由脚本调用的许多 Roblox API 方法也有自己的附属微调器标签,可以提供有用的信号。

脚本内存使用率

当您写脚本时,可能会发生内存泄漏,因为垃圾收集器无法在不再使用时正确释放内存。泄漏在服务器上特别普遍,因为它们可以连续多日在线,而客户端会话要短得多。

开发者控制台 中的下列内存值可能表示需要进一步调查的问题:

  • Lua堆 - 高或增长的消耗建议存在内存泄漏。
  • 实例数量 - 一致增长的实例数量表明您的代码中的某些实例未被垃圾收集。
  • 放置脚本记忆 - 提供通过脚本分解内存使用量的脚本。

常见问题

  • 保留连接连接 - 引擎永远不会收集到连接到实例和内部引用的任何值的事件。因此,连接到连接实例、连接函数和参考值内的事件和代码的活连接在事件发射后仍然出于内存回收器的范围。

    尽管当属于的实例被摧毁时事件会断开,但一个常见的错误是假设这适用于 Player 对象。用户离开体验后,引擎不会自动销毁他们的代表 Player 对象和角色模型,因此连接到角色模型下的 Player 对象和实例,例如 Player.CharacterAdded ,如果你在脚本中没有断开它们,仍然会消耗内存。这可能导致服务器上的数百个用户加入和离开体验时间过程中的非常大的内存泄漏。

  • 表格 - 将对象插入表,但当不再需要时未删除它们会导致不必要的内存消耗,尤其是当它们加入时跟踪用户数据的表。例如,以下代码示例每次用户加入时创建一张添加用户信息的表:

    示例

    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。

  • 使用适应物理步骤 - 适应步骤动态调整物理机制的计算速率,在某些情况下减少物理更新的频率。

  • 减少机制复杂度 * 尽可能减少组装中的物理约束或接口数量。

    • 减少机制内的自我碰撞量,例如通过对布娃娃肢应用限制或无碰撞约束来防止它们相互碰撞。
  • 减少网格精确碰撞精度的使用 * 对于那些小或不能互动的对象,用户很少会注意到差异,请使用盒子忠实度。

    • 对于小型到中型的对象,可以使用盒子或船体的忠实度,取决于形状。

    • 对于大而复杂的对象,尽可能使用隐形部件构建自定义碰撞。

    • 对于不需要碰撞的对象,禁用碰撞并使用箱子或船体完整性,因为碰撞几何仍然存储在内存中。

    • 您可以在 Studio 中通过切换 碰撞精度 从 3D 视窗右上角的 视觉选项 选项卡来渲染碰撞几何图形用于调试目的。

      或者,您可以将 CollisionFidelity = Precise 过滤器应用到 浏览器 ,该浏览器显示所有精确匹配的网格部件的数量,并允许您轻松选择它们。

    • 要了解有关如何选择平衡精度和性能要求的碰撞保真选项的深入教程,请参阅设置物理和渲染参数

微型调试器范围

涉及范围关联计算
物理步骤整体物理计算
世界步每帧采取的分散物理步骤

物理内存使用率

物理移动和碰撞检测会消耗内存。网格部件具有 CollisionFidelity 属性,决定用于评估网格碰撞边界的方法。

常见问题

默认和精确的碰撞检测模式消耗的内存比其他两种模式(精度较低的碰撞形状)要多得多。

如果您在 物理部件 下看到高的内存消耗水平,您可能需要探索降低体验中对象的 碰撞精度

如何应对

要减少用于碰撞一致性的内存使用:

  • 对于不需要碰撞的零件,通过设置 BasePart.CanCollideBasePart.CanTouchBasePart.CanQuery 将其碰撞禁用为 false
  • 使用 CollisionFidelity 设置减少碰撞的稳定性。Box 拥有最低的内存开销,而 DefaultPrecise 一般来说更昂贵。
    • 一般来说,设置任何小型锚定部件的碰撞精度为 Box 是很安全的。
    • 对于非常复杂的大型网格,您可能想使用箱子碰撞精度较小的对象来构建自己的碰撞网格。

人型怪物

Humanoid 是一个提供广泛功能给玩家和非玩家角色(NPC)的类。虽然强大,一个 Humanoid 带有重大的计算成本。

常见问题

  • 将所有人形状态类型在 NPC 上启用 - 启用某些 HumanoidStateTypes 会有性能成本。禁用任何不需要为您的 NPC 使用的内容。例如,除非你的 NPC 要攀爬梯子,否则禁用 Climbing 状态是安全的
  • 经常使用机器人模型来实现、修改和重生模型 * 这可能需要引擎处理,尤其是如果这些模型使用 分层服装 。这也可能在经常重生的虚拟形象体验中特别具有问题。
    • 微型调试器 中,长时间的 更新不支持快速集群 标签(超过 4 ms)经常是虚拟形象初始化/修改触发过多无效化的信号。
  • 在不需要的情况下使用人形物在 - 不移动的静态 NPC 一般没有需要 Humanoid 类。
  • 从服务器播放大量 NPC 的动画 - 在服务器上运行的 NPC 动画需要在服务器上模拟并复制到客户端。这可能是多余的开销。

缓解

  • 在客户端播放 NPC 动画 - 在具有大量 NPC 的体验中,考虑在客户端上创建 Animator 并本地运行动画。这降低了服务器的负载和不必要的复制需求。它还可以实现额外的优化(例如仅为靠近角色的 NPC 播放动画)。
  • 使用性能友好的替代方案来替换人形怪物 - NPC 模型不一定需要包含一个人形怪物。
    • 对于静态 NPC,使用简单的 AnimationController ,因为它们不需要移动,只需要播放动画。
    • 对于移动 NPC,考虑实现自己的移动控制器并使用 AnimationController 进行动画,根据 NPC 的复杂程度进行。
  • 禁用未使用的人形状态 - 使用 Humanoid:SetStateEnabled() 仅为每个人形启用必要状态。
  • 频繁重生的泳池 NPC 模型 - 而不是完全摧毁一个 NPC,将 NPC 发送到一组不活跃的 NPC 中。这样,当需要重生新 NPC 时,您可以简单地重新激活池中的一个 NPC。这个过程被称为泳池化,可以最大限度地减少字符需要被实例化的次数。
  • 仅在用户靠近时生成 NPC,否则不要生成 NPC,当用户离开范围时收集它们 - 不要在用户不在范围内时生成 NPC,当用户离开范围时收集它们
  • 避免在实例化后对虚拟形象层次结构进行更改 - 对虚拟形象层次结构进行某些修改会对性能产生重大影响。有些优化可用:
    • 对于自定义程序动画,不要更新 JointInstance.C0JointInstance.C1 属性。相反,更新 Motor6D.Transform 属性。
    • 如果需要将任何 BasePart 对象附加到虚拟形象上,请在虚拟形象的等级之外进行操作 Model

微型调试器范围

涉及范围关联计算
步骤人形化人型控制和物理学
步骤动画人形和动画师动画
更新无效快速集群与创建或修改虚拟形象相关

渲染中

客户每次花费的时间的重要部分是在当前框架中渲染场景。服务器不进行任何渲染,因此该部分仅供客户端使用。

发起调用

一个绘制调用是从引擎到 GPU 渲染某些内容的一组指令。绘制调用有明显的开销。一般来说,每帧的调用次数越少,渲染框架所花的计算时间就越少

您可以在 Studio 中查看多少绘制调用正在发生,以及与 渲染统计 > 时间 项目有多少关联。您可以通过按 渲染统计 在客户端按Shift查看F2

在特定帧中需要在场景中绘制的对象越多,对 GPU 的绘制调用就越多。然而,Roblox 引擎使用名为 实例化 的过程将具有相同纹理特性的相同网格合并为单个绘制调用。具体来说,具有相同 MeshId 的多个网格在单次绘制调用中处理时:

其他常见问题

  • 过度的对象密度 - 如果大量对象聚集在高密度,那么渲染场景的这一区域需要更多的绘制调用。如果您在查看地图的某部分时发现帧率下降,这可能是一个好信号,表明该区域的对象密度过高。

    像图标、纹理和粒子等对象不能批量处理,并且会导致额外的绘制调用。在场景中给予这些对象类型特别关注。特别是,将属性更改为 ParticleEmitters 可能会对性能产生巨大影响。

  • 错过实例化机会 - 经常情况下,场景会包含同一网格重复多次,但每个网格复制的网格或纹理资产ID各不相同。这会导致实例化失败,可能会导致不必要的调用。

    这个问题的一个常见原因是,当整个场景一次导入时,而不是单个资产被导入到 Roblox 然后在导入后复制以组装场景时。

  • 过度复杂的对象 - 虽然不像调用数量那样重要,但场景中三角形的数量会影响渲染框架所需的时间。拥有非常大数量的非常复杂网格的场景是一个常见问题,场景拥有 MeshPart.RenderFidelity 属性设置到 Enum.RenderFidelity.Precise 上的网格太多也是一个常见问题

  • 过度投射阴影 - 处理阴影是一项昂贵的过程,包含大量投射阴影的地图(或大量受阴影影响的小部件)可能会出现性能问题。

  • 高透明度溢出 - 将部分透明度的对象放置在一起,强迫引擎多次渲染重叠像素,可能会影响性能。了解有关识别和修复此问题的更多信息,请参阅删除分层透明度

缓解

  • 实例化相同的网格并减少唯一网格数量 - 如果您确保所有相同网格都具有相同的基础资产 ID,引擎可以识别并在单次绘制调用中渲染它们。请确保只在地图上上传每个网格一次,然后在工作室中复制它们,而不是导入整个大型地图,这可能导致相同的网格拥有不同的内容 ID 并被引擎识别为独特资产。是一个有用的对象重用机制。
  • 筛选 - 筛选描述了淘汰不因最终渲染框架而因素化的对象的调用过程。默认情况下,引擎会跳过镜头视野外的对象的绘制调用(锥体筛选),但不会跳过由其他对象遮蔽视野的对象的绘制调用(遮蔽筛选)。如果你的场景有很多绘制调用,考虑在运行时动态实现自己的额外筛选,例如应用以下常见策略:
    • 隐藏 MeshPartBasePart 那些远离相机或设置的东西
    • 对于室内环境,实现一个房间或门户系统,隐藏目前没有被任何用户使用的对象。
  • 降低渲染精度 - 将渲染精度设置为 自动性能 。这允许网格回落到较少复杂的替代方案,从而减少需要绘制的多边形数量。
  • 禁用适当部件和灯光对象上的阴影投射功能 - 通过选择性禁用阴影投射属性对灯光对象和部件上的阴影投射功能,场景中的阴影复杂度可以降低。这可以在编辑时或运行时动态完成。一些例子如下:
    • 使用 BasePart.CastShadow 属性来禁用在小部件上投射阴影的可能性,因为阴影很可能不会被发现这特别有效,当仅应用于远离用户相机的部件时。

    • 如果可能,关闭移动对象上的阴影。

    • 在轻型实例上禁用 Light.Shadows,对象不需要投射阴影。

    • 限制光实例的范围和角度。

    • 使用更少的轻型实例。

微型调试器范围

涉及范围关联计算
准备并执行最终渲染
执行/场景/计算光照演算灯光网格和阴影更新
轻型网格处理器声量光网格更新
阴影地图系统阴影地图
执行/场景/更新视图为渲染和粒子更新做准备
执行/场景/渲染视图渲染和后期处理

网络和复制

网络和复制描述了数据在服务器和连接的客户端之间发送的过程。信息在每一帧之间发送从客户端到服务器,但更大的信息需要更多的计算时间。

常见问题

  • 过度的远程交通 - 通过 RemoteEventRemoteFunction 对象发送大量数据或过于频繁地调用它们可能会导致每帧收到的包的处理时间大量消耗。常见错误包括:

    • 复制每个不需要复制的框架中的数据。
    • 无任何机制限制用户输入数据的复制。
    • 发送超过需要的数据。例如,发送玩家购买物品时整个库存而不是购买的物品的详细信息
  • 创建或移除复杂实例树 - 当在服务器上对数据模型进行更改时,它会复制到连接的客户端。这意味着创建和摧毁在运行时创建和摧毁像地图一样的大规模实例架构可能需要非常多的网络带宽。

    一个常见的罪魁是动画编辑器插件在模型中保存的复杂动画数据。如果在游戏发布前这些内容未被删除,并且动画模型定期克隆,将会复制大量不必要的数据。

  • 服务器端渐进服务 - 如果 TweenService 用于渐进对象服务端,渐进的属性每帧都会复制到每个客户端。这不仅导致青少年因客户端延迟波动而紧张,还会导致大量不必要的网络流量。

缓解

您可以使用以下策略来减少不必要的复制:

  • 避免一次通过远程事件发送大量数据 .相反,只在更低频率发送必要数据。例如,对于角色的状态,在它更改时进行复制,而不是每帧。
  • 拆分复杂的实例树 像地图一样,将它们分块加载以分配这些在多个框架上复制的工作。
  • 清理动画元数据 ,特别是导入后的动画目录的骨架。
  • 限制不必要的实例复制 ,尤其是在服务器不需要知道正在创建的实例的情况下其中包括:
    • 视觉效果,例如爆炸或魔法法术冲击波。服务器只需要知道位置来确定结果,而客户端可以在本地创建视觉效果。
    • 第一人称物品视图模型。
    • 在客户端而不是服务器上显示中间对象。

微型调试器范围

涉及范围关联计算
处理包块处理来自网络的包裹,例如事件调用和属性更改
分配带宽并运行发送器服务器上相关的输出事件

资产内存使用率

创建者可以使用的最高影响机制来改善客户端内存使用是启用实例传输

实例流媒体

实例流选择性加载不需要的数据模型部分,可以大大减少加载时间并提高客户端在面临内存压力时防止崩溃的能力。

如果您遇到内存问题且已禁用实例流式传输,请考虑升级您的体验以支持它,尤其是如果您的 3D 世界很大。实例传播基于 3D 空间的距离,因此更大的世界自然更受益。

如果实例传播已启用,您可以提高它的攻击性。例如,考虑:

  • 减少使用持久的 传输完整性
  • 减少 流媒体半径

了解有关流媒体选项和其优势的更多信息,请参阅 流媒体属性

其他常见问题

  • 资产复制 - 一个常见的错误是多次上传相同的资产,导致不同的资产ID。这可能会导致同一内容多次加载到内存。
  • 过大的资产量 - 即使资产不相同,也会有情况错过重复使用相同资产并节省内存的机会。
  • 音频文件 - 音频文件可能是一个令人惊讶的内存使用贡献者,特别是如果你一次加载所有它们而不仅加载你需要的一部分体验的内容。有关策略,请参阅加载时间
  • 高分辨率纹理 - 图形内存消耗对纹理的大小无关,而是与纹理中的像素数有关。
    • 例如,1024x1024像素纹理消耗的图形内存是 512x512纹理的四倍。
    • 上传到 Roblox 的图像会被转换为固定格式,因此没有通过将图像上传到颜色模型与每像素相关的更少字节的内存优惠。同样,在上传或从不需要它的图像中删除 Alpha 通道之前压缩图像,可以减少图像在磁盘上的大小,但也不会提高或仅略微提高内存使用率。虽然引擎会自动在某些设备上降低纹理分辨率,但降级的程度取决于设备特性,过高的纹理分辨率仍然可能导致问题。
    • 您可以通过在 开发者控制台 扩展 图形图像 类别来识别给定纹理的图形内存消耗。

缓解

  • 只上传资产一次 - 在对象之间重复使用相同的资产 ID,确保相同的资产,特别是网格和图像,不会单独多次上传。
  • 查找并修复重复资产 - 寻找具有相同网格部件和纹理,且多次上传到不同ID的资产。
    • 虽然没有 API 自动检测资产的相似性,但你可以在你的位置收集所有图像资产 ID(通过手动操作或脚本),下载它们,并使用外部比较工具进行比较。
    • 对于网格零件,最好的策略是采取独特的网格 ID 并按尺寸组织它们,手动识别重复零件。
    • 而不是使用不同颜色的独立纹理,上传单个纹理,然后使用 SurfaceAppearance.Color 属性将各种颜色调色应用到它上。
  • 单独导入地图中的资产 - 而不是一次导入整个地图,单独导入地图中的资产并重构它们。3D导入器不会对网格进行重复,因此如果你导入了一个大型地图,包含许多单独的地板砖,那么每块地板砖都会导入为单独的资产(即使它们是重复的)。这可能会导致线程中的性能和内存问题,因为每个网格都被视为独立并占用内存和绘制调用。
  • 限制图像像素 不超过必要数量。除非图像占用了屏幕上大量的物理空间,否则通常最多需要 512x512 像素。大多数小型图像应小于 256x256 像素。
  • 使用修边表格 以确保在 3D 地图中最大限度地重复纹理。有关如何创建修边表的步骤和例子,请参阅创建修边表

加载时间

许多体验实现了自定义加载屏幕,使用 ContentProvider:PreloadAsync() 方法请求资产,以便图像、音效和网格在背景下下载。

这种方法的优势是,它可以让你确保体验的重要部分无需弹出即可完全加载。然而,一个常见的错误是过度使用此方法来预加载超过实际需要的更多资产。

一个恶劣实践的例子是加载整个**Workspace。虽然这可能会阻止纹理弹出,但它会显著增加加载时间。

相反,只在必要情况下使用 ContentProvider:PreloadAsync(),这包括:

  • 加载屏幕的图像。
  • 体验菜单中的重要图像,例如按钮背景和图标。
  • 启动或生成区域中的重要资产。

如果您必须加载大量资产,我们建议您提供 跳过加载 按钮。