本指南概述了在游戏中高效有效使用实例流媒体的几种技术。虽然设计流媒体游戏或将非流媒体游戏转换为流媒体没有“放之四海而皆准”的解决方案,但遵循这些高层步骤将使您大致达到目标。
流媒体属性
一旦在Studio中为 Workspace 对象切换 StreamingEnabled,请将其相关属性设置为以下推荐值:
| 属性 | 推荐 |
|---|---|
| EnableSLIMAvatars | 使用 Enabled 在适当情况下将标准模型头像呈现为轻量级的、动画转换体。有关更多信息,请参见SLIM 头像。 |
| ModelStreamingBehavior | 使用 Improved 启用 Models 及其 BasePart 后代的最高效流媒体。 |
| StreamingIntegrityMode | 使用 PauseOutsideLoadedArea 平衡游戏的完整性,而不必过多或不必要地暂停。 |
| StreamingMinRadius | 使用默认值 64 最大限度提高引擎为低端设备缩小游戏的能力。 |
| StreamingTargetRadius | 使用默认值 1024 以在高端设备上为玩家提供良好的可见度和平衡合理的内存占用。 |
| StreamOutBehavior | 使用 Opportunistic 允许客户端积极回收内容,从而显著减少内存使用并帮助防止内存不足崩溃。 |
模型细节级别
Model.LevelOfDetail 有助于用轻量级复合或替身网格填充未流式传输的 Model 内容,使世界在视觉上看起来完整。SLIM(可扩展轻量级交互模型)复合网格尤其有效,因为玩家通常无法区分 SLIM 网格与完全流式传输的原件。
为了获得最佳结果:
- 将空间和逻辑上相关的部件分组,例如所有汽车的部件。
- 将每个模型的空间范围保持在 ~64 立方钉以内,以增加整个实际模型一起流式传输的可能性。如果一个模型的范围非常大,将其分解为更小的模块化模型,并对每个模型应用适当的 LevelOfDetail。
模型结构
除了设置模型细节级别,您的 Models 的结构和设置对流媒体性能有显著影响。当您构建或转换现有游戏时:
使用原子模型进行逻辑分组 — 当脚本需要访问模型中的所有部件时,将其 ModelStreamingMode 设置为 Atomic。这使得客户端脚本可以安全访问模型内部的实例,而无需过度使用 WaitForChild()(尽管这些脚本仍必须在整个原子模型上使用 WaitForChild())。
最小化持久模型 — 持久模型在加入后加载并且从不流式传输,永久占用内存。仅在模型必须始终可用并可供脚本访问时,将其 ModelStreamingMode 设置为 Persistent。
SLIM 头像
在当前流式传输区域外的平台头像默认不可见,但启用 Workspace.EnableSLIMAvatars 会在适当情况下将标准模型头像呈现为轻量级、动画的替代品。事实上,引擎:
- 在实际头像模型流式传输出时呈现一个 SLIM 版本。
- 根据流媒体半径内可用资源在 SLIM/实际之间进行切换,因为 SLIM 模型的渲染成本通常显著低于全分辨率模型。
- 根据场景重要性和带宽限制 SLIM 动画的速率。
头像细节级别系统对头像具有特定优化。以下描述了 SLIM 将处理或不处理的内容:
脚本模式
以下脚本模式最常受到流媒体的影响。正确的策略取决于代码的意图,因此每个模式根据需要列出多个选项。
直接索引后代
使用 . 运算符索引 Workspace 的后代时,如果路径中的任何实例未被当前流媒体化,将会抛出错误。FindFirstChild()、FindFirstChildWhichIsA() 和 FindFirstChildOfClass() 也适用,若子项未流媒体化则返回 nil。
后代查找local house1 = workspace:FindFirstChild("House1") -- 如果“House1”未流媒体化,则为 nillocal 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 引用的零件可能尚未存在,即使在流媒体区域内也是如此。
通过 RemoteEvent 或 RemoteFunction 从服务器到客户端发送零件/模型引用要求该实例必须先复制到接收客户端。通过字符串发送实例路径也具有相同的问题,因为该路径可能无法解析为客户端上不存在的位置:
客户端脚本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.ChildRemoved 和 CollectionService 信号如 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() doif home.Settings.Owner.Value == player.Name thenreturn homeendend
如果需要完整枚举,请在服务器上执行扫描并通过 RemoteEvent 将结果传递给玩家(如有需要)。
空间查询
客户端空间查询如 WorldRoot:Raycast()、WorldRoot:GetPartBoundsInBox() 和 Model:GetBoundingBox() 仅反映流媒体化的内容。这是否成问题取决于查询的用途。
使用服务器进行必须反映整个世界的查询,例如检查玩家是否可以看到远处目标的射线投射。
其他模式
以下模式也可能适用,应仔细考虑:
由 3D 对象父级的 Sound 或 AudioPlayer 在该对象流媒体化时停止。对于应在流媒体上下文中持续的环境音频,请将发射器父级设置为持久模型或非流媒体容器。
游戏内UI对象,如 BillboardGui 或 SurfaceGui,以及视觉效果如 Beams 或 Highlights,如果其附着物或附加组件流媒体化,则会停止渲染。这可能是预期行为,但您应进行验证。
BasePart.Touched 事件、ProximityPrompts、DragDetectors 和 ClickDetectors 对于未在客户端流媒体化的相关部件/模型的玩家无效。如果必须在任何范围内进行交互,则模型需要是持久的,或者交互需要不同的机制。
对于 PathfindingService 和客户端路径寻路,路径寻找器在客户端上仅查看流媒体化的几何体,并且可能通过存在于服务器上的障碍物。请参阅此处的策略。
实际测试条件
一旦脚本被更新,请彻底测试游戏。流媒体错误通常仅在流媒体区域的边缘或过渡期间显现,因此仅在生成点或目标半径附近测试是不够的。
在将 Workspace.StreamingTargetRadius 设置为其最小值(64)时进行测试。一些流媒体错误仅在流媒体区域较小时出现。
玩遍整个游戏的完整遍历模式,在遥远区域之间传送,并在离开后重新访问区域。这些是最能行使流媒体进出功能的场景。
使用流媒体调试覆盖监视活动流媒体设置、当前加载的区域和运行时流媒体状态。
查看输出窗口和开发者控制台以查找错误,因为许多脚本模式会产生错误,而不是静默的误行为。特别注意attempt to index nil with ... 形式的错误,这通常表明缺少 WaitForChild() 调用。
装备和激活 Tools,开火武器,并触发不同的游戏交互。

