伺服器權限技術

*此內容是使用 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 -- 部件現在在資料模型中;在此之後,任何非仿真訪問的更改都將出現錯誤
-- 部件在客戶端立即存在,並將與伺服器進行調和
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 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可能不再有效。

鏡像動畫邏輯

與任何核心遊戲玩法邏輯一樣,控制動畫的邏輯必須在伺服器和客戶端之間保持同步,否則可能會出現錯誤預測和抖動行為。請參見模擬同步以了解如何通過在客戶端和伺服器上初始化的ModuleScript內的RunService:BindToSimulation()綁定函數的模式。

避免緩存軌跡

在非伺服器權限腳本中的一個常見模式是在加載時緩存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 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 是我們在美國及其他國家地區的部分註冊與未註冊商標。