服务器授权技术

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

本指南概述了使用 服务器授权模型 创建高质量、流畅的多人游戏的各种技术。

预测实例创建(实例拼接)

实例拼接允许客户端脚本在 RunService:BindToSimulation() 回调内部预测性地创建 实例。客户端立即创建 Instance,而无需等待服务器的往返;当服务器的权威副本到达时,客户端创建的实例和服务器的权威副本将合并为一个。从你的脚本的角度来看,Instance 立即存在并与服务器一致。

实例拼接在需要尽快在客户端上可见和活动的情况下非常有用。虽然服务器最终会复制客户端所需的任何实例(连同它们对世界的任何影响),但这一过程由于服务器通信会导致至少一次往返延迟。例如,发射火箭发射器和创建物理约束——如果不使用拼接,客户端将看到火箭在远处突然出现,或者在新的约束复制到它们时出现一些抖动。

技术行为

实例拼接通过在客户端和服务器上生成相同的确定性 GUID 来工作。GUID 来源于四个输入:要创建的 Instance 的类型、源的身份(见下文)、当前仿真帧和每个脚本重置的调用计数器。

如果客户端和服务器在输入上达成一致,它们将生成匹配的 GUID,拼接将成功。

实现

要利用实例拼接,请在 ModuleScript 中从客户端服务器要求的 BindToSimulation() 回调内部调用 Instance.new()Instance:Clone()Instance.fromExisting()。在你的端不需要其他任何东西;系统会自动处理 GUID 分配和调和。

你可以自由地在实例被附加到 DataModel 之前设置非 仿真访问 属性,例如 NameSizeParent

仿真 (ModuleScript) - 在 BindToSimulation() 回调中创建实例

local RunService = game:GetService("RunService")
local Simulation = {}
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local part = Instance.new("Part")
part.Name = "PredictedPart"
part.Size = Vector3.new(2, 2, 2)
part.Parent = workspace -- 该部分现在在 DataModel 中;在此之后的任何非仿真访问更改将出错
-- 部分立即在客户端上存在并将与服务器调和
end)
end
return Simulation

Instance:Clone()Instance.fromExisting() 在源实例已被复制到客户端和服务器时正确拼接;双方都从匹配的源 GUID 克隆并生成匹配的预测 GUID。

仿真 (ModuleScript) - 在 BindToSimulation() 回调中克隆实例

local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- 复制的实例
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- 克隆的层次结构与服务器的权威副本拼接
end)
end
return Simulation

位置平滑

您可以通过渲染与正在被仿真的对象不同的对象来可视化平滑位置,以平滑不同步的同步对象的视觉效果。

  1. 使仿真对象不可见。
  2. 创建一个渲染器对象作为无质量、不可碰撞、仅用于视觉的克隆以跟踪被仿真的对象。
  3. 将脚本附加到渲染器对象,该脚本平滑跟踪不可见的仿真对象的位置。渲染和仿真之间的这种分离使您可以改变渲染器对象的位置,从而创建视觉上平滑的体验。

在以下示例 Script 中,渲染的对象(父级)平滑跟踪仿真对象。渲染对象总是稍微“落后”于仿真对象,这通常是可以的,但在某些情况下可能会引起不满。

使用渲染器部件平滑跟踪 BasePart 位置

local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
-- 要平滑跟踪的对象
local smoothTarget:BasePart = Workspace.SimulatedPart
-- 将要平滑的可视对象
local renderer:BasePart = script.Parent
-- 平滑时间;越小越快
local smoothTime = 0.07
-- 存储平滑位置所需的数据
local smoothVelocity = Vector3.new()
-- 禁用渲染器对象的物理
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- 平滑跟踪目标对象
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

Soccer 示例游戏使用这种技术的变体来更智能地开启和关闭足球的位置信息平滑。具体来说,当模拟球“跳”得足够远离渲染球时,足球才会平滑其位置。这种方法提供了两全其美的效果:在正常条件下,足球没有视觉延迟,只有在模拟球意外跳到新位置后,游戏才会平滑插值其位置,这可能是由于网络伪影或服务器端更改导致的。

编写动画代码

在服务器授权下,当服务器纠正错误预测时,客户端的仿真可以被 回滚和重新仿真。在回滚期间,动画状态被倒回,这意味着 AnimationTrack 在早期帧中缓存的内容可能不再有效。

镜像动画逻辑

与任何核心游戏逻辑一样,用于控制动画的逻辑必须在服务器和客户端之间保持同步,否则可能会出现错误预测和抖动行为。请参阅 仿真同步 以了解一个模式,通过 RunService:BindToSimulation() 绑定函数,在一个在客户端和服务器上初始化的 ModuleScript 中。

避免轨迹缓存

在非服务器授权脚本中,常见的模式是在加载时缓存 AnimationTrack 对象并无限期重用它们。在服务器授权游戏中,如果服务器纠正错误预测并且客户端用纠正的数据倒回/重放其仿真,则该模式将失败。如果你的脚本仍持有已停止或替换轨道的引用,像 AdjustWeight()AdjustSpeed() 的调用将对不再可视的轨道操作。

在客户端缓存轨道(不可靠)

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
-- 缓存动画轨道
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

与其保留轨道对象,不如存储动画 ID(或 Animation 实例),并在需要与之交互时查询 Animator 获取实时轨道。此时有两个 API 可用:

  • Animator:GetTrackByAnimationId() — 返回特定动画 ID 的当前活动轨道;如果没有活动动画,则返回 nil。当你知道你在寻找哪个特定动画时使用此方法。
  • Animator:GetPlayingAnimationTracks() — 返回所有活动轨道(播放中、渐出或暂停)。当你需要迭代所有活动的轨道时使用,例如,以停止所有动画或根据一些标准查找轨道。

ModuleScript 名为 CustomAnimate,位于 ReplicatedStorage

CustomAnimate

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- 存储动画引用(未加载的轨道)
local animations = {
WalkForward = ReplicatedStorage.Animations.WalkForward,
}
local function getOrLoadTrack(animator: Animator, animation: Animation): AnimationTrack
local track = animator:GetTrackByAnimationId(animation.AnimationId)
if not track then
track = animator:LoadAnimation(animation)
end
return track
end
CustomAnimate.SyncAnimations = function(character)
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
RunService:BindToSimulation(function(dt: number)
local walkTrack = getOrLoadTrack(animator, animations.WalkForward)
if not walkTrack.isPlaying then
walkTrack.Looped = true
walkTrack.Priority = Enum.AnimationPriority.Core
walkTrack:Play()
end
walkTrack:AdjustSpeed(1 + math.cos(time()))
end)
end
return CustomAnimate

播放声音和视觉效果

在预测的仿真中,可以触发效果或声音,以供客户端预测将发生但在服务器上从未发生的事件。渲染系统应准备好“撤销”任何错误预测的效果。例如,客户端可能预测手榴弹爆炸并触发粒子效果,但如果另一个玩家拆除了手榴弹,客户端应该隐藏粒子效果。

渲染预测仿真的一个好策略是在仿真循环中同步状态机模式,并在渲染步骤函数中渲染状态的变化。以下示例使用状态机模式模拟手榴弹:

用于跟踪手榴弹的简单状态机(ModuleScript)

local Workspace = game:GetService("Workspace")
local module = {}
module.GrenadeStates = {
Idle = 0,
Lit = 1,
Exploded = 2,
Defused = 3,
}
module.GrenadeExplodeTime = 3.0
module.Initialize = function(grenade)
RunService:BindToSimulation(function(deltaTime)
-- 初始化空的手榴弹状态
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- 增加手榴弹计时器
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- 引爆点燃的手榴弹
if grenadeState == module.GrenadeStates.Lit then
if timer >= module.GrenadeExplodeTime then
grenadeState = module.GrenadeStates.Exploded
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
end
end)
end
return module

在设置了前述状态机的情况下,您可以在基于同步手榴弹状态的单独脚本中,在 RunService.RenderStepped 连接中渲染手榴弹效果:

基于同步手榴弹状态渲染粒子和声音

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- 高亮实例以指示手榴弹状态
local highlight = Instance.new("Highlight")
highlight.Parent = grenade
highlight.FillTransparency = 1
highlight.OutlineTransparency = 1
highlight.DepthMode = Enum.HighlightDepthMode.Occluded
RunService.RenderStepped:Connect(function(deltaTime: number)
local grenadeState = grenade:GetAttribute("State")
local grenadeTimer = grenade:GetAttribute("Timer")
-- 如果手榴弹点燃,则发射点燃的粒子
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- 如果手榴弹刚刚爆炸,则播放爆炸发射器
if previousGrenadeState ~= grenadeState then
if grenadeState == Simulation.GrenadeStates.Exploded and grenadeTimer < 0.2 then
grenade.ExplosionEmitter:Emit(100)
grenade.ExplosionSound:Play()
end
previousGrenadeState = grenadeState
end
-- 根据状态和时间更改手榴弹的高亮颜色
if grenadeState == Simulation.GrenadeStates.Lit then
highlight.FillColor = Color3.fromRGB(255, 0, 0)
highlight.FillTransparency = 1 - (grenadeTimer / Simulation.GrenadeExplodeTime)
elseif grenadeState == Simulation.GrenadeStates.Idle then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Exploded then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Defused then
highlight.FillColor = Color3.fromRGB(0, 255, 125)
highlight.FillTransparency = 0.5
end
end)

设计网络延迟

某些游戏机制相比其他机制更适合网络多人游戏。玩家在另一个玩家执行操作到接收到该玩家的输入之间总会有一些延迟。创建一个超流畅的多人游戏的最佳方法是考虑这些限制来设计游戏。

例如,玩家移动的加速度较慢的游戏看起来比加速度较高的游戏更平滑,因为由于玩家输入的网络延迟造成的位置差异会比高加速度游戏中的小。

再举个例子,玩家通过按输入立即触发大爆炸的机制会比如果爆炸在输入后延迟(就像点燃引线一样)更多网络伪影。这将使重仿真放在引线效果上,而不是爆炸效果,这样的网络伪影不那么显著。

预测其他玩家输入

默认情况下,Roblox不会将每个客户端的输入转发到其他每个客户端。这是否适合你的游戏取决于其设计:

  • 对于基本类人的移动,默认行为意味着其他玩家角色的移动不会从权威服务器状态推导,因此其他玩家角色不会错误预测,但是会稍微渲染在过去。
  • 另一方面,在赛车游戏中,默认行为意味着客户端不会知道其他玩家是否正在使用油门或其他输入,因此其他汽车可能会在本地玩家的后面,即使它们实际上在前面。为了解决这个问题,你可以在服务器上通过 属性 存储玩家输入,并使用 RunService:BindToSimulation() 在客户端操作这些同步属性,这在下面的代码示例和 Racing 模板中得到了演示。这种方法让你可以将属性用作你的仿真的输入,以完全复制玩家的输入。
在属性中存储玩家输入(ModuleScript)

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local module = {}
module.storePlayerInput = function(player:Player, humanoidRootPart:BasePart)
local inputContext:InputContext = player.PlayerGui.InputContext
local throttle = inputContext.DefuseAction:GetState()
humanoidRootPart:SetAttribute("Throttle", throttle)
-- 将其他输入写入属性...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- 将输入从服务器转发到所有客户端
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- 将本地玩家输入写入属性
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- 使用属性作为游戏的输入
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- 将油门应用于玩家的车辆
end
end
end)
end)
return module

调试

有一些新工具和技术可用于调试一个服务器授权的游戏。

服务器授权可视化工具

CtrlShiftF6(Windows)或 ShiftF6(Mac)可以打开 Studio 的服务器授权可视化工具,该工具显示多个关键信息:

详细信息描述
实例预测成功率过去 8 秒内正确预测的实例的百分比。
输入接受率按时到达服务器的所有玩家输入的百分比。迟到输入会降低此数字。
客户端-服务器步长差客户端和服务器之间的帧数,包括客户端的加入时间。该数字的稳定性代表你与服务器的连接稳定性。
RCC 心跳 FPS服务器上仿真的帧率。如果该数字低于 59,服务器无法跟上仿真,游戏质量将下降。
预测实例计数你的客户端正在 预测 的实例数量。
输入丢失原因计数

服务器因每个原因丢弃输入的次数:

  • [x] 过旧 — 输入迟到了,意味着你的网络变差了或者客户端无法跟上仿真。
  • [x] 顺序错误 — 发生了网络错误,导致输入被重新排序并丢弃。
  • [x] 缓冲区已满 — 服务器无法缓冲你的输入。要么你的网络突然改善,要么服务器无法跟上仿真。

仿真半径

依靠自动预测 (Enum.PredictionMode.Automatic) 时,可以通过在 Studio 设置中启用 区域已启用 来可视化角色周围的预测半径(Windows 上按 AltS;Mac 上按 S)。绿色圆柱指示角色周围预测的范围,其半径根据设备的性能特征增大或缩小。

正在运行服务器授权的玩家角色周围的仿真半径
©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。