技术与转换

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

本指南概述了在游戏中高效有效使用实例流媒体的几种技术。虽然设计流媒体游戏或将非流媒体游戏转换为流媒体没有“放之四海而皆准”的解决方案,但遵循这些高层步骤将使您大致达到目标。

流媒体属性

一旦在Studio中为 Workspace 对象切换 StreamingEnabled,请将其相关属性设置为以下推荐值:

属性推荐
EnableSLIMAvatars使用 Enabled 在适当情况下将标准模型头像呈现为轻量级的、动画转换体。有关更多信息,请参见SLIM 头像
ModelStreamingBehavior使用 Improved 启用 Models 及其 BasePart 后代的最高效流媒体。
StreamingIntegrityMode使用 PauseOutsideLoadedArea 平衡游戏的完整性,而不必过多或不必要地暂停。
StreamingMinRadius使用默认值 64 最大限度提高引擎为低端设备缩小游戏的能力。
StreamingTargetRadius使用默认值 1024 以在高端设备上为玩家提供良好的可见度和平衡合理的内存占用。
StreamOutBehavior使用 Opportunistic 允许客户端积极回收内容,从而显著减少内存使用并帮助防止内存不足崩溃。

模型细节级别

Model.LevelOfDetail 有助于用轻量级复合或替身网格填充未流式传输的 Model 内容,使世界在视觉上看起来完整。SLIM(可扩展轻量级交互模型)复合网格尤其有效,因为玩家通常无法区分 SLIM 网格与完全流式传输的原件。

为了获得最佳结果:

  • 将空间和逻辑上相关的部件分组,例如所有汽车的部件。
  • 对包含静态网格和部件的模型,将 LevelOfDetail 设置为 SLIM。包含蒙皮网格、在运行时被修改或播放动画的模型不支持此设置。
  • 将每个模型的空间范围保持在 ~64 立方钉以内,以增加整个实际模型一起流式传输的可能性。如果一个模型的范围非常大,将其分解为更小的模块化模型,并对每个模型应用适当的 LevelOfDetail

模型结构

除了设置模型细节级别,您的 Models 的结构和设置对流媒体性能有显著影响。当您构建或转换现有游戏时:

  • 使用原子模型进行逻辑分组 — 当脚本需要访问模型中的所有部件时,将其 ModelStreamingMode 设置为 Atomic。这使得客户端脚本可以安全访问模型内部的实例,而无需过度使用 WaitForChild()(尽管这些脚本仍必须在整个原子模型上使用 WaitForChild())。

  • 最小化持久模型持久模型在加入后加载并且从不流式传输,永久占用内存。仅在模型必须始终可用并可供脚本访问时,将其 ModelStreamingMode 设置为 Persistent

  • 分解容器模型 — 一个常见的非流媒体模式是一个包含许多 NPC、道具或类似分组的巨大 Model。在流媒体下,容器模型会降低流媒体效率,并且不适合模型细节级别,而后者在实例紧密分组时表现最佳。将容器模型分解为更小的模型,其中包含物理上接近或逻辑上相关的部件。

  • 扁平化深层嵌套模型层次结构 — 将一个持久模型嵌套在一个原子模型内有效地强迫原子模型表现为持久。扁平的层次结构在流媒体下更易于理解。

SLIM 头像

在当前流式传输区域外的平台头像默认不可见,但启用 Workspace.EnableSLIMAvatars 会在适当情况下将标准模型头像呈现为轻量级、动画的替代品。事实上,引擎:

  • 在实际头像模型流式传输出时呈现一个 SLIM 版本。
  • 根据流媒体半径内可用资源在 SLIM/实际之间进行切换,因为 SLIM 模型的渲染成本通常显著低于全分辨率模型。
  • 根据场景重要性和带宽限制 SLIM 动画的速率。

头像细节级别系统对头像具有特定优化。以下描述了 SLIM 将处理或不处理的内容:

脚本模式

以下脚本模式最常受到流媒体的影响。正确的策略取决于代码的意图,因此每个模式根据需要列出多个选项。

直接索引后代

使用 . 运算符索引 Workspace 的后代时,如果路径中的任何实例未被当前流媒体化,将会抛出错误。FindFirstChild()FindFirstChildWhichIsA()FindFirstChildOfClass() 也适用,若子项未流媒体化则返回 nil

后代查找

local house1 = workspace:FindFirstChild("House1") -- 如果“House1”未流媒体化,则为 nil
local door = workspace.House1.Door -- 如果“House1”或“Door”未流媒体化,则损坏

一种类似的模式是在 Player.CharacterAdded 连接中直接访问 Humanoid 或其他角色后代。在流媒体下,角色模型在所有后代复制之前被父级定位到 Workspace,因此直接索引失败。

角色后代

local Players = game:GetService("Players")
local player = Players.LocalPlayer
player.CharacterAdded:Connect(function(character)
local humanoid = character.Humanoid
end)

如果脚本无法在没有实例的情况下进行,则使用 WaitForChild() 等待它:

后代查找

local house1 = workspace:WaitForChild("House1")
local door = house1:WaitForChild("Door")

远程发送的实例

RemoteEvent/RemoteFunction 信号及其所指实例独立传输,因此信号可以在客户端到达之前,实例尚未存在——或者实例可能根本不存在。两个可能的原因包括:

  • 在流媒体下,服务器上创建零件/模型与复制到客户端之间可能存在轻微延迟。实际上,通过 RemoteEvent/RemoteFunction 引用的零件可能尚未存在,即使在流媒体区域内也是如此。

  • 通过 RemoteEventRemoteFunction 从服务器到客户端发送零件/模型引用要求该实例必须先复制到接收客户端。通过字符串发送实例路径也具有相同的问题,因为该路径可能无法解析为客户端上不存在的位置:

    客户端脚本

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
    remoteEvent.OnClientEvent:Connect(function(data)
    local checkpoint = data.checkpoint -- 如果 "checkpoint" 未流媒体化则出错
    local level = workspace.Levels[data.levelPath] -- 如果路径未流媒体化则出错
    end)

如果接收客户端脚本需要该实例才能继续,则在使用之前包含 WaitForChild()。注意,如果实例从未流媒体化,这可能会无限期地挂起,因此考虑在 WaitForChild() 的第二个参数中添加超时。

客户端不同步

客户端的不同步应被视为异常,而非标准设计模式。引入仅客户端副本或在本地重新父级实例可能会引发严重问题。审查您的代码,查看哪些地方依赖于这些类型的更改在客户端持久化。

例如,将一个实例从 ReplicatedStorage 本地重新父级到 Workspace 可以使该实例有资格被流媒体化。同样,从 ReplicatedStorage 本地克隆一个实例(Instance:Clone())到 Workspace 会创建一个仅客户端的副本,该副本不再是服务器复制管道的一部分,并且将不会从原服务器拥有的实例接收属性更新。

同样的概念适用于对服务器拥有的对象调用 Instance:Destroy()。这将本地删除该实例,但服务器仍然拥有它,因此当它有资格时将以其原始状态重新流媒体化。

积极流媒体

当可以预见玩家下一个目的地时,通过服务器端调用 Player:RequestStreamAroundAsync() 来流媒体化临时区域以进行临时加载,或在有限的基础上使用 Player:AddReplicationFocus() 以使某些区域保持加载,直到显式释放。

例如,当玩家角色即将通过 CFrame 更改到远处的另一个玩家的房子时,您可以预取目标区域,以尽量减少突入现象并提供更流畅的过渡。以下脚本展示了如何触发一个客户端到服务器的远程事件以使用预取方法移动玩家角色。如果预取请求在函数返回时成功,则目标位置周围的最小半径应存在于客户端。

服务器脚本 - 传送玩家角色

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local teleportEvent = ReplicatedStorage:WaitForChild("TeleportEvent")
local function teleportPlayer(player, teleportTarget)
-- 请求在目标位置周围流媒体化
player:RequestStreamAroundAsync(teleportTarget)
-- 传送角色
local character = player.Character
if character and character.Parent then
local currentPivot = character:GetPivot()
character:PivotTo(currentPivot * CFrame.new(teleportTarget))
end
end
-- 当客户端触发远程事件时调用传送函数
teleportEvent.OnServerEvent:Connect(teleportPlayer)

实例属性读取

一旦实例流媒体化,其属性更新将不再复制到该客户端。读取诸如 BasePart.Position 的属性仍然成功,但返回的是最后复制的值,这可能是任意陈旧的。


local Players = game:GetService("Players")
local player = Players.LocalPlayer
-- 如果 "target" 已流媒体化,则 Position 可能已过时
local dist = (target.Position - player.Character.HumanoidRootPart.Position).Magnitude

将逻辑移动到服务器,因为服务器端脚本随时查看所有实例。这通常是距离检查和其他位置敏感的逻辑的最可靠选项。

在关键路径上等待

某些非流媒体游戏通过将地图从 ReplicatedStorage 克隆到 Workspace 加载其地图,然后在客户端等待它,然后取消加载屏幕并发出准备信号。在流媒体下,这会无限期挂起——客户端的角色尚未生成,因此没有复制焦点,空间地图实例从未流媒体化。

移动加载屏幕逻辑,使其不依赖于特定空间实例的存在,例如在角色生成且周围的区域已流媒体化后发出准备信号。

信号变更处理

Instance.ChildAdded/Instance.ChildRemovedCollectionService 信号如 GetInstanceAddedSignal()GetInstanceRemovedSignal() 也会在流媒体化时触发,与真实生成/删除无法区分。接收的脚本无法仅凭信号判断差异,因此假设信号与“真实”事件相对应的逻辑需要更新。

审核脚本,检查可能在流媒体化触发时破坏或显著变化的任何信号监听器。例如,如果您在敌方 NPC 初次生成到世界时播放音频或视觉效果,则在第一次生成时将每个敌人分配一个属性,如Spawned,并跳过在未来敌人流媒体中重播相同的音频/效果。

属性跟踪

local CollectionService = game:GetService("CollectionService")
local TAG_NAME = "Enemy"
CollectionService:GetInstanceAddedSignal(TAG_NAME):Connect(function(enemy)
if not enemy:GetAttribute("Spawned") then
-- 在敌人第一次生成时设置 "Spawned" 属性
enemy:SetAttribute("Spawned", true)
-- 播放此初次生成的音频/视觉效果
playSpawnEffects(enemy)
end
end)

集合的迭代

客户端集合的迭代如 Instance:GetChildren()Instance:GetDescendants() 仅返回流媒体化的后代子集。这即使在父级本身始终被复制时也适用,例如直接位于 Workspace 下的 Folder 的空间后代流入流出。


local Players = game:GetService("Players")
local player = Players.LocalPlayer
-- 文件夹 "Homes" 始终被复制,但其子项流进流出
-- 此循环可能会错过当前未流媒体化的房屋
for _, home in workspace.Homes:GetChildren() do
if home.Settings.Owner.Value == player.Name then
return home
end
end

如果需要完整枚举,请在服务器上执行扫描并通过 RemoteEvent 将结果传递给玩家(如有需要)。

空间查询

客户端空间查询如 WorldRoot:Raycast()WorldRoot:GetPartBoundsInBox()Model:GetBoundingBox() 仅反映流媒体化的内容。这是否成问题取决于查询的用途。

使用服务器进行必须反映整个世界的查询,例如检查玩家是否可以看到远处目标的射线投射。

其他模式

以下模式也可能适用,应仔细考虑:

  • 由 3D 对象父级的 SoundAudioPlayer 在该对象流媒体化时停止。对于应在流媒体上下文中持续的环境音频,请将发射器父级设置为持久模型或非流媒体容器。

  • 游戏内UI对象,如 BillboardGuiSurfaceGui,以及视觉效果BeamsHighlights,如果其附着物或附加组件流媒体化,则会停止渲染。这可能是预期行为,但您应进行验证。

  • BasePart.Touched 事件、ProximityPromptsDragDetectorsClickDetectors 对于未在客户端流媒体化的相关部件/模型的玩家无效。如果必须在任何范围内进行交互,则模型需要是持久的,或者交互需要不同的机制。

  • 对于 PathfindingService 和客户端路径寻路,路径寻找器在客户端上仅查看流媒体化的几何体,并且可能通过存在于服务器上的障碍物。请参阅此处的策略。

实际测试条件

一旦脚本被更新,请彻底测试游戏。流媒体错误通常仅在流媒体区域的边缘或过渡期间显现,因此仅在生成点或目标半径附近测试是不够的。

  • 在将 Workspace.StreamingTargetRadius 设置为其最小值(64)时进行测试。一些流媒体错误仅在流媒体区域较小时出现。

  • 玩遍整个游戏的完整遍历模式,在遥远区域之间传送,并在离开后重新访问区域。这些是最能行使流媒体进出功能的场景。

  • 使用流媒体调试覆盖监视活动流媒体设置、当前加载的区域和运行时流媒体状态。

  • 查看输出窗口和开发者控制台以查找错误,因为许多脚本模式会产生错误,而不是静默的误行为。特别注意attempt to index nil with ... 形式的错误,这通常表明缺少 WaitForChild() 调用。

  • 装备和激活 Tools,开火武器,并触发不同的游戏交互。

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