提高性能

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

此页面描述了常见的性能问题以及缓解这些问题的最佳实践。

脚本计算

Luau 代码中的昂贵操作处理时间较长,因此可能影响帧率。除非以并行方式执行,Luau 代码是同步运行的,并会阻塞主线程,直到遇到一个可以让线程挂起的函数。

常见问题

缓解

  • RunService 事件上有选择地调用代码,限制使用到高频调用至关重要的情况(例如,更新摄像机)。您可以在其他事件中或在循环中以较低频率执行大多数其他代码。
  • 使用 task.wait() 将大或昂贵的任务拆分,以将工作分布到多个帧中。
  • 识别并优化不必要的昂贵操作,并在计算上昂贵但不需要访问数据模型的任务中使用 多线程
  • 某些服务器端脚本可以利用 本地代码生成,这是一个简单的标志,可以将脚本编译为机器代码而不是字节码。

MicroProfiler 作用域

作用域相关计算
RunService.PreRender在 PreRender 事件上执行的代码
RunService.PreSimulation在 Stepped 事件上执行的代码
RunService.PostSimulation在 Heartbeat 事件上执行的代码
RunService.Heartbeat在 Heartbeat 事件上执行的代码

有关如何使用 MicroProfiler 调试脚本的更多信息,请查看 debug 库,其中包括标记特定代码和进一步增加特异性的函数,例如 debug.profilebegindebug.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。

  • 使用自适应物理步进 - 自适应步进动态调整物理机制的计算速率,允许在某些情况下更少地更新物理计算。

  • 减少机制复杂性

    • 在可能的情况下,减少组件中的物理约束或接头数量。
    • 减少机制内部的自我碰撞,例如,通过对绳索肢体施加限制或无碰撞约束来防止它们互相碰撞。
  • 减少网格的精确碰撞保真度的使用

    • 对于小或不互动的对象,在用户很少注意到差异的情况下,使用盒子保真度。

    • 对于小到中等规模的对象,根据形状使用盒子或外壳保真度。

    • 对于大型和非常复杂的对象,尽可能使用不可见部件构建自定义碰撞。

    • 对于不需要碰撞的对象,禁用碰撞并使用盒子或外壳保真度,因为碰撞几何仍保存在内存中。

    • 您可以通过在 3D 视口的右上角小部件中切换 碰撞保真度,为调试目的渲染碰撞几何。

      或者,您可以在 Explorer 应用 CollisionFidelity=PreciseConvexDecomposition 过滤器,以显示所有具有精确保真度的网格部件的计数,并允许您轻松选择它们。

    • 有关选择平衡您的精确度和性能要求的碰撞保真度选项的深入指南,请参见 设置物理和渲染参数

MicroProfiler 作用域

作用域相关计算
physicsStepped整体物理计算
worldStep每帧执行的离散物理步骤

物理内存使用

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

常见问题

默认和精确的碰撞检测模式消耗的内存显著高于两个其他较低保真度碰撞形状的模式。

如果您在 PhysicsParts 下看到高水平的内存消耗,您可能需要考虑减少游戏中对象的 碰撞保真度

如何缓解

要减少碰撞保真度使用的内存:

  • 对于不需要碰撞的部件,通过将 BasePart.CanCollideBasePart.CanTouchBasePart.CanQuery 设置为 false 来禁用它们的碰撞。
  • 使用 CollisionFidelity 设置降低碰撞保真度。Box 具有最低的内存开销,而 DefaultPrecise 通常更昂贵。
    • 通常可以安全地将任何小锚定部件的碰撞保真度设为 Box
    • 对于非常复杂的巨大网格,您可能希望使用更小的具有盒子碰撞保真度的对象构建自己的碰撞网格。

人形

Humanoid 是一个提供广泛功能给玩家和非玩家角色(NPC)的类。尽管功能强大,Humanoid 的计算成本却很高。

常见问题

  • 在 NPC 上留所有 HumanoidStateTypes 启用 - 留下某些 HumanoidStateTypes 启用会产生性能开销。禁用 NPC 不需要的状态。例如,除非您的 NPC 要爬梯子,否则可以安全地禁用 Climbing 状态。
  • 频繁实例化、修改和重生具有 Humanoids皮肤化MeshParts 模型
    • 这可能会给引擎处理带来密集负担,特别是如果这些模型使用 分层服装。在角色频繁重生的游戏中,这种问题尤其突出。
    • 在 MicroProfiler 中,较长的 updateInvalidatedFastClusters 标签(超过 4 毫秒)通常表明头像实例化/修改触发过度失效。
  • 在不需要的情况下使用 Humanoids - 静态的、不会移动的 NPC 通常不需要 Humanoid 类。
  • 从服务器为大量 NPC 播放动画 - 在服务器上运行的 NPC 动画需要在服务器上模拟并复制给客户端。这可能会造成不必要的开销。
  • 执行不必要的大小和缩放更改 - 大小/缩放更改导致 FastCluster 被重建。如果您发现与 FastCluster 相关的性能问题,请尽量在游戏中减少此操作。类似地,其他属性更改也可能导致 FastCluster 被重建,因此总体上尽量减少这些更改。

缓解

  • 在客户端播放 NPC 动画 - 在拥有大量 NPC 的游戏中,考虑在客户端创建 Animator 并在本地运行动画。这减少了服务器负载和不必要的复制需求。它还使额外优化成为可能(例如,仅为接近角色的 NPC 播放动画)。
  • 使用性能友好的替代品代替 Humanoids - NPC 模型并不一定需要包含人形对象。
    • 对于静态 NPC,使用简单的 AnimationController,因为它们不需要移动,只需播放动画。
    • 对于移动 NPC,考虑实现自己的运动控制器,并根据 NPC 的复杂性使用 AnimationController 进行动画。
  • 禁用未使用的人形状态 - 使用 Humanoid:SetStateEnabled() 仅启用每个 Humanoid 所需的状态。
  • 对频繁重生的 NPC 模型进行池化 - 不要完全摧毁 NPC,而是将其发送到一个非活动 NPC 的池中。这样,当需要重生新 NPC 时,您可以简单地从池中重新激活一个 NPC。这种过程称为池化,可以最小化角色被实例化的次数。
  • 仅当用户接近时生成 NPC - 当用户不在范围内时不要生成 NPC,当用户离开其范围时控制 NPC 的出现。
  • 避免在实例化后对头像层次结构进行更改 - 对头像层次结构的某些修改会产生显著的性能影响。有一些优化可用:

MicroProfiler 作用域

作用域相关计算
stepHumanoid人形控制和物理
stepAnimation人形和动画员的动画
updateInvalidatedFastClusters与实例化或修改头像相关

渲染

客户端在每帧上花费的时间中很大一部分用于渲染当前帧的场景。服务器不进行任何渲染,因此此部分仅适用于客户端。

绘制调用

绘制调用是引擎向 GPU 发出的指令集,用于渲染某些内容。绘制调用有显著的开销。一般而言,每帧的绘制调用越少,渲染一帧所花费的计算时间就越少。

您可以通过 Studio 中的 渲染统计计时 项查看当前发生的绘制调用数量。您可以通过按 ShiftF2 在客户端查看 渲染统计

在给定的帧中,需要绘制的对象越多,GPU 中的绘制调用就越多。然而,Roblox 引擎利用一种称为 实例化 的过程,将具有相同纹理特征的相同网格合并为单个绘制调用。具体来说,当多个具有相同 MeshContent 的网格在单个绘制调用中处理时:

其他常见问题

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

    像贴花、纹理和粒子这样的对象无法很好地批处理,并会引入额外的绘制调用。特别关注场景中的这些对象类型。特别是,ParticleEmitters 的属性变化可能会对性能产生显著影响。

  • 错失实例化机会 - 通常,场景中包括相同网格的多次重复,但每个网格的网格或纹理资产 ID 不同。这会阻止实例化并导致不必要的绘制调用。

    产生此问题的一个常见原因是当整个场景一次性导入时,而不是在 Roblox 中导入单个资产,然后在导入后复制它们以组装场景。

    甚至一个像这样的简单脚本也可以帮助您识别具有相同名称但使用不同网格 ID 的网格部件:

    for _,descendant in workspace:GetDescendants() do
    if descendant:IsA("MeshPart") then
    print(descendant.Name .. ", " .. descendant.MeshId)
    end
    end

    输出(启用 堆栈行)可能如下所示。重复的行表示重复使用相同的网格,这是好的。唯一的行不一定是坏的,但根据您的命名方案,可能表示游戏中存在重复的网格:

    LargeRock, rbxassetid://106420009602747 (x144) -- good
    LargeRock, rbxassetid://120109824668127
    LargeRock, rbxassetid://134460273008628
    LargeRock, rbxassetid://139288987285823
    LargeRock, rbxassetid://71302144984955
    LargeRock, rbxassetid://90621205713698
    LargeRock, rbxassetid://113160939160788
    LargeRock, rbxassetid://135944592365226 -- all possible duplicates
  • 过度的对象复杂性 - 尽管绘制调用的数量并不如重要,但场景中的三角形数量确实会影响每帧的渲染时间。具有非常大量复杂网格的场景是一个常见问题,同时,许多网格上设置了 MeshPart.RenderFidelity 属性为 Precise

  • 过多的阴影投射 - 处理阴影是一个昂贵的过程,包含大量阴影光源的地图(或受到阴影影响的许多小部件)可能会导致性能问题。

  • 高透明度过绘 - 将部分透明的物体放置在相互靠近的位置,迫使引擎多次渲染重叠像素,这会影响性能。有关识别和修复此问题的更多信息,请参见 删除分层透明度

  • 不必要的皮肤网格部件运动 - 没有 Humanoid 的模型中的皮肤网格部件被使用空间组织的 FastClusters 分组。当这些网格部件移动时,它们必须不断添加到这些空间集群中并从中移除,迫使集群重建并影响性能。

    • 一种高效的解决方法是在模型中嵌入 Humanoid 的存在。Humanoid 的存在会覆盖默认的空间聚类行为,强制整个模型使用单个统一的 FastCluster。因此,位置更新不再需要集群重建,从而缓解了性能瓶颈。此技术应专门保留给预计将移动的网格部件,因为它可能引入内存开销并抵消空间优化的好处。我们建议始终在进行这些更改后分析您的游戏。有关额外信息,请参见 人形性能提示
  • Model 中部件过多 - 如果模型中的部件过多,由于可能会导致部分属性的变化需要完全重建,可能会导致更频繁的重建。当使用 FastCluster 时找到模型中部件的合适平衡。

缓解

  • 实例化相同的网格并减少唯一网格的数量 - 如果您确保所有相同的网格具有相同的底层资产 ID,则引擎可以识别并在单个绘制调用中渲染它们。确保在地图中只上传每个网格一次,然后在 Studio 中复制它们以供重用,而不是将大型地图作为整体导入,这可能导致相同的网格具有不同的内容 ID,并被引擎识别为独特资产。 是用于对象重用的有用机制。

  • 剔除 - 剔除描述了排除不影响最终渲染帧的对象的绘制调用的过程。默认情况下,引擎会跳过超出相机视野(视锥体剔除)或被其他对象遮挡的部分、网格和地形的绘制调用(遮挡剔除)。在某些情况下,例如室内环境,您可能能够实现房间或传送门系统,并手动剔除对象以进一步减少绘制调用或整体计算负载。

  • 减少模型的细节等级 - 启用 实例流 并将您的世界模型的 LevelOfDetail 属性设置为 SLIM,以便在与相机的距离增加时为模型渲染 优化的轻量级 SLIM 网格

  • 减少头像的细节等级 - 启用实例流并将 Workspace.EnableSLIMAvatars 设置为渲染平台头像为 优化的轻量级 SLIM 表示,在与相机的距离增加时保持完整动画支持。

  • 降低渲染保真度 - 将 MeshPart.RenderFidelity 设置为 AutomaticPerformance。这允许网格退回到较不复杂的替代品,可以减少需要绘制的多边形数量。

  • 在适当的部件和光源上禁用阴影投射 - Roblox 引擎会随着客户端图形质量级别的降低自动降低阴影质量,最终在质量级别低于 4 时完全禁用阴影。但是,您可以选择性地在光源和部件上禁用阴影投射属性,在启用阴影时提高性能,并增加阴影保持启用的可能性。一些您可以在编辑时或在运行时动态进行的优化示例包括:

    • 使用 BasePart.CastShadow 属性禁用不太可能可见阴影的小部件的阴影投射。这种策略在应用于远离用户相机的部件时尤其有效。

    • 尽可能禁用移动对象的阴影。

    • 在不需要投射阴影的光实例上禁用 Light.Shadows

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

    • 使用更少的光实例。

    • 考虑在特定范围外或在室内环境的每个房间中禁用光源。

MicroProfiler 作用域

作用域相关计算
Prepare and Perform整体渲染
Perform/Scene/computeLightingPerform光网格和阴影更新
LightGridCPU体素光网格更新
ShadowMapSystem阴影映射
Perform/Scene/UpdateView渲染准备和粒子更新
Perform/Scene/RenderView渲染和后处理

网络和复制

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

常见问题

  • 过多远程流量 - 通过 RemoteEventRemoteFunction 对象发送大量数据或非常频繁地调用它们会导致每帧处理传入数据包消耗大量 CPU 时间。常见错误包括:

    • 每帧复制不需要复制的数据。
    • 在用户输入时复制数据,没有任何节流机制。
    • 分派超出要求的数据。例如,在玩家购买物品时发送玩家整个库存,而不是仅仅发送购买物品的详细信息。
  • 复杂的实例树的创建或移除 - 当对服务器上的数据模型进行更改时,会将其复制给连接的客户端。这意味着在运行时创建和销毁大型实例层次结构(如地图)可能会非常网络密集。

    这里一个常见的罪魁祸首是由 动画编辑器 插件在模型中保存的复杂动画数据。如果在游戏发布之前未将其移除,并且被频繁克隆的动画模型则会导致大量不必要的数据被复制。

  • 服务器端 TweenService - 如果在服务器端使用 TweenService 对对象进行插值,则每帧都会将插值的属性复制到每个客户端。这不仅会导致由于客户端延迟波动导致插值变得不稳定,还会造成大量不必要的网络流量。

缓解

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

  • 避免通过远程事件一次性发送大量数据。转而仅以较低频率发送必要的数据。例如,对于角色状态,在其变化时复制,而不是每帧复制。
  • 将复杂的实例树,例如地图,分块加载,以将处理这些实例的复制工作分配到多个帧中。
  • 清理动画元数据,尤其是在导入后清理模型的动画目录。
  • 限制不必要的实例复制,尤其是在服务器不需要了解创建的实例的情况下。这包括:
    • 视觉效果,例如爆炸或魔法飞溅。服务器只需了解位置以确定结果,而客户端可以在本地创建视觉效果。
    • 第一人称物品视图模型。
    • 在客户端而不是服务器上插值对象。

MicroProfiler 作用域

作用域相关计算
ProcessPackets处理传入网络数据包,如事件调用和属性更改
Allocate Bandwidth and Run Senders与服务器相关的外发事件

资产内存使用

提高客户端内存使用的重要机制是启用 实例流

实例流

实例流选择性地加载出不需要的数据模型部分,从而显著减少加载时间并提高客户端在内存压力下防止崩溃的能力。

如果您遇到内存问题且已禁用实例流,请考虑更新您的游戏以支持它,特别是如果您的 3D 世界很大。实例流是基于 3D 空间中的距离的,因此较大的世界自然会从中受益更多。

如果启用了实例流,您可以提高其激进性。例如,请考虑:

  • 在可能的情况下减少 Enum.ModelStreamingMode.Persistent 的使用。如果您使用它作为兼容性措施,您可能需要更新您的脚本。
  • 减少 Workspace.StreamingMinRadiusWorkspace.StreamingTargetRadius

有关流选项及其好处的更多信息,请参见 流属性

其他常见问题

  • 资产重复 - 常见错误是多次上传相同资产,导致不同的资产 ID。这可能导致相同内容在内存中被加载多次。

  • 资产数量过多 - 即使资产不相同,在某些情况下也会错失重复使用相同资产并节省内存的机会。

  • 音频文件 - 音频文件可能是内存使用的意外贡献者,特别是如果您一次性将所有音频文件加载到客户端,而不是仅加载您所需的部分游戏音频。有关策略,请参见 加载时间

  • 高分辨率纹理 - 纹理的图形内存消耗与纹理在磁盘上的大小无关;纹理中的像素数量决定内存使用。例如,1024x1024 像素纹理消耗的图形内存是 512x512 纹理的四倍。

    上传至 Roblox 的图像会被转码为固定格式,因此在使用与每像素较少字节关联的颜色模型上传图像时并没有内存收益。类似地,在上传之前压缩图像或从不需要的图像中移除 alpha 通道可以减少在磁盘上的图像大小,但不会改善内存使用。

    当游戏加载时,引擎会自动从低质量纹理开始,然后根据可用设备内存、与相机的距离、纹理占用的屏幕空间以及其他因素逐步提高质量。尽管如此,按照策略调整纹理的大小也可以改善游戏的内存使用。

缓解

  • 只上传资产一次 - 在对象中重用相同的资产 ID,并确保相同的资产(尤其是网格和图像)不会重复上传。

  • 查找并修复重复的资产 - 查找多个相同 ID 上传的网格部件和纹理。

    • 尽管没有 API 可以自动检测资产的相似性,但您可以收集地点中所有的图像资产 ID(手动或通过脚本),下载它们,并使用外部比较工具对比。
    • 对于网格部件,最佳策略是将独特的网格 ID 按大小组织,以手动识别重复项。
    • 不要对不同颜色使用单独的纹理,而是上传单个纹理,并使用 SurfaceAppearance.Color 属性应用各种色调。
  • 分开导入地图中的资产 - 不要一次性导入整个地图,而是单独导入并重建地图中的资产。导入器不会对网格进行去重,因此如果您导入一个包含许多单独地砖的大型地图,那么每一个砖块都会作为单独的资产导入(即使它们是重复的)。这可能会导致后续的性能和内存问题,因为每个网格都会被视为单独的实体,并占用内存和绘制调用。

  • 限制图像像素数,不要超过必要的数量。除非图像在屏幕上占用大量物理空间,通常最多需要 512x512 像素。大多数小图像应小于 256x256 像素。

  • 使用修整图,以确保在 3D 地图中最大限度地重用纹理。有关如何创建修整图的步骤和示例,请参见 创建修整图

    您也可以考虑使用精灵图来将多个较小的 UI 图像作为单个图像加载。然后,您可以使用 ImageLabel.ImageRectOffsetImageLabel.ImageRectSize 来显示该图的部分。

加载时间

许多游戏实现自定义加载屏幕,并使用 ContentProvider:PreloadAsync() 方法请求资产,以便在后台下载图像、声音和网格。

这种方法的优势在于,它可以确保游戏的重要部分完全加载而不会出现图像闪烁。然而,常见的错误是过度利用该方法预加载实际不需要的资产。

一个糟糕的做法示例是加载整个 Workspace。虽然这可能防止纹理弹出,但它显著增加了加载时间。

另一个类似的做法是利用 ContentProvider.RequestQueueSize 来确保所有请求的资产已完成加载。然而,这也存在显著增加加载时间的问题,并且由于其波动性,通常是一个不可靠的方法。

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

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

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

©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。