Kỹ thuật quyền điều khiển máy chủ

*Nội dung này được dịch bằng AI (Beta) và có thể có lỗi. Để xem trang này bằng tiếng Anh, hãy nhấp vào đây.

Hướng dẫn này phác thảo các kỹ thuật khác nhau để tạo ra những trò chơi nhiều người chơi chất lượng cao, mượt mà bằng cách sử dụng mô hình quyền điều khiển máy chủ.

Tạo phiên bản dự đoán (kết nối phiên bản)

Kết nối phiên bản stitching cho phép các script phía client tạo ra Instances dự đoán trong các callback của RunService:BindToSimulation(). Client tạo ra Instance ngay lập tức mà không phải chờ đợi phản hồi từ máy chủ; khi bản sao quyền điều khiển của máy chủ đến, phiên bản được tạo ra bởi client và bản sao quyền điều khiển của máy chủ sẽ được gộp lại thành một. Từ quan điểm của script của bạn, Instance tồn tại ngay lập tức và nhất quán với máy chủ.

Kết nối phiên bản rất hữu ích trong các trường hợp phiên bản cần phải được nhìn thấy và hoạt động trên client càng sớm càng tốt. Mặc dù máy chủ cuối cùng sẽ sao chép bất kỳ phiên bản nào mà client cần (cùng với bất kỳ hiệu ứng nào mà chúng có trên thế giới), quá trình này sẽ mất ít nhất một lần phản hồi do giao tiếp với máy chủ. Ví dụ bao gồm việc bắn một khẩu rocket và tạo ra các giới hạn vật lý — nếu không có kết nối, client sẽ thấy rocket xuất hiện ở xa hơn hoặc có một chút rung lắc khi các giới hạn mới được sao chép tới họ.

Hành vi kỹ thuật

Kết nối phiên bản hoạt động bằng cách tạo ra cùng một GUID xác định trên cả client và server. GUID này được tạo ra từ bốn đầu vào: loại của Instance đang được tạo ra, danh tính của nguồn (xem bên dưới), khung mô phỏng hiện tại và một bộ đếm gọi theo từng script mà được đặt lại mỗi khung.

Nếu client và server đồng thuận về các đầu vào, chúng sẽ sản xuất các GUID khớp nhau và kết nối sẽ thành công.

Triển khai

Để sử dụng kết nối phiên bản, hãy gọi Instance.new(), Instance:Clone(), hoặc Instance.fromExisting() bên trong callback của RunService:BindToSimulation() từ một ModuleScript mà được yêu cầu trên cả client server. Không có gì khác là cần thiết từ phía bạn; hệ thống tự động xử lý việc gán GUID và hòa giải.

Bạn có thể tự do đặt các thuộc tính không phải là truy cập mô phỏng như Name, Size, hoặc Parent trên một phiên bản trước khi nó được ghép vào trong DataModel.

Mô phỏng (ModuleScript) - Tạo Phiên bản trong Callback 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 -- Phần là bây giờ trong mô hình dữ liệu; bất kỳ thay đổi nào không phải truy cập mô phỏng sẽ gặp lỗi sau điều này
-- Phần tồn tại ngay lập tức trên client và sẽ được hòa giải với máy chủ
end)
end
return Simulation

Instance:Clone()Instance.fromExisting() kết nối đúng khi phiên bản gốc đã được sao chép tới cả client và server; cả hai bên đều sao chép từ GUID gốc khớp nhau và tạo ra GUID dự đoán khớp.

Mô phỏng (ModuleScript) - Sao chép Phiên bản trong Callback BindToSimulation()
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- một Phiên bản đã được sao chép
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- Phân cấp đã sao chép được kết nối với bản sao quyền điều khiển của máy chủ
end)
end
return Simulation

Làm mượt vị trí

Bạn có thể làm mượt trực quan vị trí của các đối tượng đồng bộ không đúng dự đoán bằng cách hiển thị một đối tượng khác hơn những gì đang được mô phỏng.

  1. Làm cho đối tượng được mô phỏng trở nên vô hình.
  2. Tạo một đối tượng renderer như một bản sao không khối lượng, không va chạm, chỉ hiển thị để theo dõi đối tượng được mô phỏng.
  3. Gắn một script vào đối tượng renderer mà theo dõi mượt mà vị trí của đối tượng vô hình, được mô phỏng. Sự tách biệt giữa render và mô phỏng cho phép bạn thay đổi vị trí của đối tượng renderer để tạo ra một trải nghiệm trực quan mượt mà.

Trong ví dụ Script sau, đối tượng được hiển thị (phụ huynh) theo dõi mượt mà đối tượng được mô phỏng. Đối tượng được hiển thị luôn "đằng sau" đối tượng được mô phỏng, điều này thường là ổn nhưng có thể không mong muốn trong một số tình huống.

Theo dõi Mượt mà Vị trí BasePart với Đối tượng Renderer
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- Đối tượng để theo dõi mượt mà
local smoothTarget:BasePart = workspace.SimulatedPart
-- Đối tượng trực quan sẽ được làm mượt
local renderer:BasePart = script.Parent
-- Thời gian để làm mượt; càng nhỏ càng nhanh
local smoothTime = 0.07
-- Lưu trữ dữ liệu cần thiết để tính toán vị trí mượt mà
local smoothVelocity = Vector3.new()
-- Vô hiệu hóa vật lý của đối tượng renderer
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Theo dõi mượt mà đối tượng mục tiêu
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

Trò chơi mẫu Soccer sử dụng một biến thể của kỹ thuật này để thông minh hơn trong việc bật và tắt làm mượt vị trí cho quả bóng. Cụ thể, quả bóng chỉ làm mượt vị trí của nó khi quả bóng được mô phỏng đã "nhảy" đủ xa khỏi quả bóng được hiển thị. Cách tiếp cận này cung cấp những điều tốt nhất của cả hai thế giới: quả bóng không có độ trễ trực quan trong điều kiện bình thường, và trò chơi mượt mà nội suy vị trí của nó chỉ sau khi quả bóng được mô phỏng đã bất ngờ nhảy tới một vị trí mới, có thể do một hiện tượng mạng hoặc thay đổi bên máy chủ.

Viết mã hoạt hình

Dưới quyền điều khiển của máy chủ, mô phỏng của client có thể được hoàn tác và mô phỏng lại khi máy chủ sửa chữa một dự đoán sai. Trong quá trình hoàn tác, trạng thái hoạt hình sẽ được quay ngược lại, có nghĩa là AnimationTrack mà bạn đã lưu trữ trong các khung trước đó có thể không còn hợp lệ.

Phản chiếu logic hoạt hình

Giống như bất kỳ logic gameplay cốt lõi nào, logic để kiểm soát hoạt hình phải nhất quán giữa máy chủ và client hoặc có thể xảy ra dự đoán sai và hành vi rung lắc. Xem đồng bộ mô phỏng để biết một mẫu gắn các hàm thông qua RunService:BindToSimulation() trong một ModuleScript mà được khởi tạo trên cả client và server.

Tránh lưu trữ track

Một mô hình phổ biến trong các script không có quyền điều khiển máy chủ là lưu trữ các đối tượng AnimationTrack tại thời điểm tải và tái sử dụng chúng vô thời hạn. Mô hình này không hiệu quả trong một trò chơi có quyền điều khiển máy chủ khi máy chủ sửa chữa một dự đoán sai và client quay ngược/phát lại mô phỏng của nó với dữ liệu đã được sửa chữa. Nếu script của bạn vẫn giữ một tham chiếu đến một track đã dừng hoặc được thay thế, các cuộc gọi như AdjustWeight() hoặc AdjustSpeed() sẽ hoạt động trên một track mà không còn được hiển thị nữa.

Lưu trữ Track trên Client (Không Tin Cậy)
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")
-- Lưu trữ các track hoạt hình
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

Thay vì giữ các đối tượng track, hãy lưu trữ ID hoạt hình (hoặc các đối tượng Animation) và truy vấn Animator để lấy track trực tiếp mỗi khi bạn cần tương tác với nó. Có hai API cho việc này:

  • Animator:GetTrackByAnimationId() — Trả về track đang hoạt động hiện tại cho một ID hoạt hình cụ thể, hoặc nil nếu không có hoạt hình nào đang hoạt động với ID đó. Sử dụng điều này khi bạn biết chính xác hoạt hình nào bạn đang tìm.
  • Animator:GetPlayingAnimationTracks() — Trả về tất cả các track đang hoạt động (đang phát, ngừng chuyển, hoặc tạm dừng). Sử dụng điều này khi bạn cần lặp qua tất cả những gì đang hoạt động (ví dụ, để dừng tất cả các hoạt hình hoặc tìm các track theo một tiêu chí nào đó).

ModuleScript có tên CustomAnimate trong ReplicatedStorage:

CustomAnimate
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Lưu trữ các tham chiếu hoạt hình (không phải track đã tải)
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

Chơi âm thanh và hiệu ứng hình ảnh

Trong một mô phỏng dự đoán, có thể kích hoạt hiệu ứng hoặc âm thanh cho các sự kiện mà client dự đoán sẽ xảy ra nhưng không xảy ra trên máy chủ. Hệ thống render nên được chuẩn bị để "hoàn tác" bất kỳ hiệu ứng nào không đúng dự đoán. Ví dụ, một client có thể dự đoán rằng một quả lựu đạn nổ và kích hoạt một hiệu ứng hạt, nhưng nếu một người chơi khác đã gỡ bom lựu đạn, client nên ẩn hiệu ứng hạt.

Một chiến lược tốt cho việc render một mô phỏng dự đoán là đồng bộ hóa một mẫu máy trạng thái trong vòng lặp mô phỏng và render những thay đổi cho trạng thái trong một hàm bước render. Ví dụ sau đây mô phỏng một quả lựu đạn với mẫu máy trạng thái:

Mẫu Máy Trạng Thái Đơn Giản để Theo Dõi Một Quả Lựu Đạn (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)
-- Khởi tạo trạng thái lựu đạn rỗng
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Tăng thời gian lựu đạn
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Nổ các lựu đạn đã được thắp sáng
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

Với mẫu máy trạng thái trước đó đã được thiết lập, bạn có thể render các hiệu ứng lựu đạn trong một kết nối RunService.RenderStepped trong một script riêng biệt dựa trên trạng thái lựu đạn đã đồng bộ:

Render Hiệu Ứng và Âm Thanh Dựa Trên Trạng Thái Lựu Đạn Được Đồng Bộ
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Đối tượng làm nổi bật để chỉ ra trạng thái của lựu đạn
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")
-- Phát ra các hạt khi lựu đạn đang cháy
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Phát hiệu ứng nổ nếu lựu đạn vừa nổ
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
-- Thay đổi màu sắc làm nổi bật của lựu đạn dựa trên trạng thái và thời gian
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)

Thiết kế xung quanh độ trễ mạng

Một số cơ chế gameplay phù hợp tốt hơn với nhiều người chơi được kết nối so với các cơ chế khác. Người chơi sẽ luôn có một độ trễ giữa thời điểm một người chơi khác thực hiện một hành động và thời điểm họ nhận được đầu vào của người chơi đó. Cách tốt nhất để tạo ra một trò chơi nhiều người chơi cực kỳ mượt mà là thiết kế trò chơi của bạn với những hạn chế này trong tâm trí.

Ví dụ, một trò chơi với sự tăng tốc chậm hơn trên chuyển động của người chơi sẽ xuất hiện mượt hơn so với một trò chơi với sự tăng tốc cao hơn vì sự khác biệt về vị trí do độ trễ mạng của đầu vào người chơi sẽ ít hơn trong một trò chơi có sự tăng tốc cao hơn.

Ví dụ khác, một cơ chế gameplay mà người chơi có thể ngay lập tức kích hoạt một vụ nổ lớn bằng cách nhấn một đầu vào sẽ có nhiều hiện tượng mạng hơn so với việc vụ nổ bị trì hoãn sau đầu vào, như thể nhóm một mảnh thuốc nổ. Điều này đặt lại việc mô phỏng lên hiệu ứng nhóm thay vì hiệu ứng vụ nổ, đó là một hiện tượng mạng ít đáng chú ý hơn.

Dự đoán đầu vào của người chơi khác

Theo mặc định, Roblox không chuyển tiếp đầu vào từ mỗi client tới tất cả các client khác. Điều này có đúng với trò chơi của bạn hay không phụ thuộc vào thiết kế của nó:

  • Đối với chuyển động humanoid cơ bản, hành vi mặc định có nghĩa là chuyển động của các nhân vật người chơi khác không được ngoại suy từ trạng thái máy chủ quyền điều khiển và, do đó, các nhân vật người chơi khác sẽ không bị dự đoán sai nhưng sẽ được render hơi ở phía quá khứ.
  • Trong một trò chơi đua xe, ngược lại, hành vi mặc định có nghĩa là các client sẽ không biết liệu người chơi khác có đang tăng ga hoặc thực hiện đầu vào khác hay không, vì vậy các xe khác có thể xuất hiện phía sau người chơi địa phương ngay cả khi chúng thực sự ở phía trước. Để khắc phục điều này, bạn có thể lưu trữ đầu vào của người chơi trong các attributes trên máy chủ và hoạt động trên các thuộc tính đồng bộ đó từ phía client bằng cách sử dụng RunService:BindToSimulation() như được nêu trong mẫu mã sau đây và mẫu Racing. Cách tiếp cận này cho phép bạn sử dụng các thuộc tính như đầu vào cho mô phỏng của bạn để có được các đầu vào của người chơi hoàn toàn tái tạo.
Lưu Trữ Đầu Vào Người Chơi trong Các Thuộc Tính (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)
-- Ghi bất kỳ đầu vào nào khác vào các thuộc tính...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Chuyển tiếp các đầu vào từ máy chủ đến tất cả client
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Ghi các đầu vào của người chơi địa phương dưới dạng thuộc tính
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Sử dụng các thuộc tính như đầu vào cho trò chơi
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Áp dụng ga cho phương tiện của người chơi
end
end
end)
end)
return module

Gỡ lỗi

Có một số công cụ và kỹ thuật mới mà bạn có thể sử dụng để gỡ lỗi một trò chơi có quyền điều khiển máy chủ.

Trình hiển thị quyền điều khiển máy chủ

Nhấn CtrlShiftF6 (Windows) hoặc ShiftF6 (Mac) mở ra trình hiển thị quyền điều khiển máy chủ của Studio, hiển thị một số thông tin chính:

Chi tiếtMô tả
Tỷ lệ dự đoán phiên bản thành côngTỷ lệ phần trăm các phiên bản được dự đoán chính xác trong 8 giây qua.
Tỷ lệ chấp nhận đầu vàoTỷ lệ phần trăm tất cả các đầu vào của người chơi đến đúng thời gian trên máy chủ. Các đầu vào muộn sẽ làm giảm con số này.
Bước delta giữa client-serverSố khung giữa client và máy chủ, bao gồm thời gian tham gia của client. Độ ổn định của số này đại diện cho sự ổn định của kết nối của bạn với máy chủ.
FPS nhịp tim RCCTốc độ khung của mô phỏng trên máy chủ. Nếu con số này giảm xuống dưới 59, máy chủ không thể theo kịp mô phỏng và chất lượng trò chơi sẽ giảm.
Số lượng phiên bản được dự đoánSố lượng phiên bản mà client của bạn đang dự đoán.
Số lần lý do rơi đầu vào

Số lần máy chủ đã loại bỏ một đầu vào cho mỗi lý do:

  • [x] quá cũ — Các đầu vào đến muộn, có nghĩa là mạng của bạn bị xấu đi hoặc client không thể theo kịp mô phỏng.
  • [x] không theo thứ tự — Một lỗi mạng đã xảy ra khiến các đầu vào của bạn bị sắp xếp lại và bị loại bỏ.
  • [x] bộ đệm đầy — Máy chủ không thể đệm đầu vào của bạn. Hoặc mạng của bạn đột ngột cải thiện hoặc máy chủ không thể theo kịp mô phỏng.

Bán kính mô phỏng

Khi dựa vào dự đoán tự động (Enum.PredictionMode.Automatic), bạn có thể hình dung bán kính dự đoán xung quanh nhân vật người chơi của bạn bằng cách bật Có Khu Vực Được Bật trong cài đặt của Studio (AltS trên Windows; S trên Mac). Silanh màu xanh lá cây chỉ ra khoảng cách xung quanh nhân vật của bạn trong đó các phiên bản được dự đoán, và bán kính của nó sẽ lớn lên và nhỏ lại dựa trên các đặc điểm hiệu suất của thiết bị.

Bán kính mô phỏng xung quanh nhân vật người chơi với quyền điều khiển máy chủ đang hoạt động
©2026 Roblox Corporation. Roblox, logo Roblox và Powering Imagination là các nhãn hiệu đã đăng ký và chưa đăng ký của chúng tôi tại Hoa Kỳ và các quốc gia khác.