伺服器授權技術

*此內容是使用 AI(Beta 測試版)翻譯,可能含有錯誤。若要以英文檢視此頁面,請按一下這裡

本指南概述了使用 伺服器授權模型 創建高質量、平滑的多人遊戲的各種技術。

預測性實例創建 (實例拼接)

實例 拼接 允許客戶端腳本在 RunService:BindToSimulation() 回調中預測性地創建 Instances。客戶端立即創建該 Instance,無需等待伺服器的往返確認;當伺服器的授權副本到達時,客戶端創建的實例和伺服器的授權副本合併為一個。從你的腳本的角度來看,Instance 立即存在並且與伺服器保持一致。

實例拼接在需要實例盡快在客戶端可見和活動的情況下非常有用。雖然伺服器最終會複製客戶端所需的任何實例(以及它們對世界的任何影響),但由於伺服器通信,這個過程至少會導致一次往返延遲。例子包括發射火箭發射器和創建物理約束 — 如果沒有拼接,客戶端會看到火箭遠離他們的地方出現,或者在新約束複製到他們那時出現一些抖動。

技術行為

實例拼接通過在客戶端和伺服器上生成相同的確定性 GUID 來工作。GUID 是從四個輸入派生而來的:要創建的 Instance 的類型、來源的身份(見下文)、當前的模擬幀以及每個腳本的調用計數器,該計數器每幀重置。

如果客戶端和伺服器同意這些輸入,它們就會生成匹配的 GUID,並且拼接成功。

實現

要利用實例拼接,在 ModuleScript 中從客戶端和伺服器都必須調用的 RunService: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)

足球 示例遊戲使用這種技術的變體,更智能地開啟和關閉足球的位置平滑。具體而言,足球僅在模擬球"跳"得遠離渲染球的時候才進行平滑。這種方法提供了雙方的最佳效果:在正常情況下,足球沒有視覺延遲,並且遊戲僅在模擬球意外跳到新位置(可能是由於網路噪音或伺服器端更改)後平滑地內插其位置。

撰寫動畫代碼

在伺服器權威下,當伺服器糾正某次錯誤預測時,客戶端的模擬可以被 回滾和重新模擬。在回滾過程中,動畫狀態會被回退,這意味著 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 的當前活動舞道,如果沒有該 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() 在客戶端側操作這些同步屬性,正如以下代碼示例和 賽車 模板中所示。這種方法讓你能夠使用屬性作為你的模擬輸入,以完美複製玩家的輸入。
在屬性中存儲玩家輸入 (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 設置中的 Are Regions Enabled(Windows 上按 AltS;Mac 上按 S)來可視化你的玩家角色周圍的預測半徑。綠色圓柱體表示圍繞你的角色的實例被預測的範圍,並且其半徑會根據設備的性能特徵增大和減小。

運行伺服器權威的玩家角色周圍的模擬半徑
©2026 Roblox Corporation、Roblox、Roblox 標誌及 Powering Imagination 是我們在美國及其他國家地區的部分註冊與未註冊商標。