Kỹ thuật quyền hạ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 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 hạn máy chủ.

Tạo instance dự đoán (instance stitching)

Instance stitching cho phép các script client tạo Instances một cách dự đoán trong các callback RunService:BindToSimulation(). Client tạo Instance ngay lập tức mà không cần chờ phản hồi từ máy chủ; khi bản sao đầy đủ của máy chủ đến, instance được tạo trên client và bản sao chính thống của máy chủ được hợp nhất 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à đồng bộ với máy chủ.

Instance stitching rất hữu ích trong các trường hợp mà một instance phải được hiển thị 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ỳ instance 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 ắt hẳn sẽ phát sinh ít nhất một lần chờ phản hồi do giao tiếp máy chủ. Các ví dụ bao gồm việc bắn một tên lửa và tạo ra các ràng buộc vật lý — mà không có stitching, client sẽ thấy tên lửa hiện ra xa khỏi họ, hoặc một số sự nhấp nháy khi các ràng buộc mới sao chép đến họ.

Hành vi kỹ thuật

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

Nếu client và máy chủ đồng ý về các đầu vào, họ sẽ sản sinh ra các GUID tương ứng và quá trình stitch sẽ thành công.

Triển khai

Để sử dụng instance stitching, gọi Instance.new(), Instance:Clone(), hoặc Instance.fromExisting() bên trong callback RunService:BindToSimulation() từ một ModuleScript được yêu cầu trên cả client máy chủ. Không cần gì khác 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 thiết lập các thuộc tính không phải truy cập mô phỏng như Name, Size, hoặc Parent trên một instance trước khi nó được làm cha trong DataModel.

Mô phỏng (ModuleScript) - Tạo Instance 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 đã có trong DataModel; bất kỳ thay đổi nào không phải truy cập mô phỏng sẽ gây 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() stitch đúng khi instance nguồn đã được sao chép đến cả client và máy chủ; cả hai bên đều sao chép từ các GUID nguồn tương ứng và tạo ra các GUID dự đoán tương tự.

Mô phỏng (ModuleScript) - Sao chép Instance trong Callback BindToSimulation()

local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- một Instance đã được sao chép
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- Cấu trúc sao chép được stitch với bản sao chính thống 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 chính xác bằng cách hiển thị một đối tượng khác với đối tượng đ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 là một bản sao không khối lượng, không va chạm, chỉ có hình ảnh để theo dõi đối tượng được mô phỏng.
  3. Gắn một script vào đối tượng renderer mà sẽ theo dõi vị trí một cách mượt mà của đối tượng được mô phỏng mà không hiển thị. Sự tách biệt giữa việc hiển thị 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 render (parent) theo dõi vị trí của đối tượng được mô phỏng một cách mượt mà. Đối tượng được render luôn "phía sau" một chút so với đối tượng được mô phỏng, điều này thường là chấp nhận được 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")
local Workspace = game:GetService("Workspace")
-- Đối tượng để theo dõi mượt mà
local smoothTarget:BasePart = Workspace.SimulatedPart
-- Đối tượng hình ảnh mà 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)

Ví dụ trò chơi Soccer sử dụng một biến thể của kỹ thuật này để bật và tắt một cách thông minh việc làm mượt vị trí cho bóng đá. Cụ thể, bóng đá chỉ làm mượt vị trí của nó khi bóng được mô phỏng đã "nhảy" đủ xa khỏi bóng được render. 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: 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 bóng được mô phỏng bất ngờ nhảy đến một vị trí mới, có thể do một hiện tượng mạng hoặc thay đổi ở phía máy chủ.

Viết mã hoạt hình

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

Tương tự logic hoạt hình

Như với bất kỳ logic gameplay nào, logic để điều khiển hoạt hình phải đồng bộ giữa máy chủ và client hoặc có thể dẫn đến các dự đoán không chính xác và hành vi rung lắc. Xem đồng bộ mô phỏng để có một mẫu liên kết các hàm thông qua RunService:BindToSimulation() trong một ModuleScript được khởi tạo trên cả client và máy chủ.

Tránh lưu trữ track

Một mẫu phổ biến trong các script không có quyền hạn máy chủ là lưu trữ các đối tượng AnimationTrack trong thời gian tải và tái sử dụng chúng vô thời hạn. Mẫu này không hiệu quả trong một trò chơi có quyền hạn máy chủ khi máy chủ sửa chữa một sự dự đoán không chính xác và client quay ngược/phát lại mô phỏng 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 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ị trực quan.

Lưu trữ Tracks 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ữ animation IDs (hoặc các instance Animation) và truy vấn Animator để lấy track sống động bất cứ khi nào bạn cần tương tác với nó. Có hai API khả dụng cho điều này:

  • Animator:GetTrackByAnimationId() — Trả về track hiện tại đang hoạt động cho một animation ID 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 animation cụ thể mà bạn đang tìm kiếm.
  • Animator:GetPlayingAnimationTracks() — Trả về tất cả các track đang hoạt động (đang chơi, đang mờ đi hoặc bị tạm dừng). Sử dụng điều này khi bạn cần lặp qua mọi thứ đang hoạt động (ví dụ, để dừng tất cả hoạt hình hoặc tìm track bằng một số tiêu chí).

ModuleScript có tên là CustomAnimate trong ReplicatedStorage:

CustomAnimate

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Lưu trữ tham chiếu hoạt hình (không phải track đã được 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 thực tế không xảy ra trên máy chủ. Hệ thống hiển thị nên chuẩn bị để "hoàn tác" bất kỳ hiệu ứng nào bị dự đoán sai. Ví dụ, một client có thể dự đoán rằng một quả lựu đạn đã phát 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ỡ bỏ quả lựu đạn, client nên ẩn hiệu ứng hạt đó.

Một chiến lược tốt cho việc hiển thị trong một mô phỏng dự đoán là đồng bộ một mẫu máy trạng thái trong vòng lặp mô phỏng và hiển thị những thay đổi về trạng thái trong một hàm bước render. Ví dụ sau 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 Quả Lựu Đạn (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)
-- Khởi tạo trạng thái lựu đạn trố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)
-- Phát nổ lựu đạn đang cháy
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 đó, bạn có thể hiển thị hiệu ứng lựu đạn trong một kết nối RunService.RenderStepped trong một script riêng dựa trên trạng thái lựu đạn đã đồng bộ:

Render Hiệu Ứng Hạt và Âm Thanh dựa trên Trạng Thái Lựu Đạn Đồng Bộ Hóa

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Làm nổi bật instance để chỉ ra trạng thái 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 sáng nếu lựu đạn đang cháy
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Phát âm thanh nổ nếu lựu đạn vừa phát 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ế theo độ trễ mạng

Một số cơ chế gameplay hợp tác tốt hơn với nhiều người chơi trực tuyến hơn các cơ chế khác. Người chơi sẽ luôn có một khoảng thời gian trì hoãn giữa khi một người chơi khác thực hiện một hành động và khi 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 siêu 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 có gia tốc chậm hơn trên chuyển động của người chơi sẽ trông mượt mà hơn so với một trò chơi có gia 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ẽ thấp hơn trong một trò chơi có gia tốc cao hơn.

Ví dụ khác, một cơ chế gameplay nơi 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 nếu vụ nổ bị trì hoãn sau đầu vào, như thể bật một ngòi nổ. Điều này đặt sự tái mô phỏng vào hiệu ứng ngòi nổ thay vì hiệu ứng vụ nổ, điều này là một hiện tượng mạng ít rõ ràng hơn.

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

Mặc định, Roblox không chuyển tiếp đầu vào từ mỗi client đến tất cả các client khác. Điều này đúng cho 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 nhân vật 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 chính thống của máy chủ và, do đó, các nhân vật người chơi khác sẽ không bị dự đoán sai mà sẽ hiển thị hơi ở phía sau.
  • 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 các người chơi khác đang tăng tốc hay nhập đầu vào khác, vì vậy các xe khác có thể xuất hiện phía sau người chơi địa phương mặc dù thực sự chúng đang ở phía trước. Để giảm thiểu vấn đề này, bạn có thể lưu trữ đầu vào của người chơi trong attributes trên máy chủ và hoạt động trên những thuộc tính đồng bộ đó bên phía client bằng cách sử dụng RunService:BindToSimulation() như thể hiện trong mẫu mã sau và mẫu Đua xe. 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ó đầy đủ các đầu vào của người chơi được sao chép.
Lưu trữ Đầu Vào của 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 đầu vào từ máy chủ đến tất cả cá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 đầ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 throttle 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 hạn máy chủ.

Trình hiển thị quyền hạn máy chủ

Nhấn CtrlShiftF6 (Windows) hoặc ShiftF6 (Mac) mở trình hiển thị quyền hạn máy chủ của Studio, cho thấy một số thông tin quan trọng:

Chi tiếtMô tả
Tỷ lệ thành công dự đoán instanceTỷ lệ phần trăm các instance dự đoán đúng trong 8 giây vừa 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 đến muộn sẽ làm giảm số này.
Delta bước client-serverSố frame giữa client và máy chủ, bao gồm cả thời gian gia nhập của client. Độ ổn định của số này đại diện cho độ ổn định của kết nối của bạn tới máy chủ.
FPS nhịp tim RCCTốc độ khung hình của mô phỏng trên máy chủ. Nếu số này giảm xuống dưới 59, máy chủ không thể theo kịp mô phỏng và trò chơi sẽ giảm chất lượng.
Số lượng instance đã dự đoánSố lượng instance mà client của bạn đang dự đoán.
Tỷ lệ lý do bị loại bỏ đầu vào

Số lần máy chủ đã loại bỏ đầu vào vì 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 đã xấu đi hoặc client không thể theo kịp mô phỏng.
  • [x] ngoài thứ tự — Một lỗi mạng đã xảy ra làm cho 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 nhiên được 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ể hiển thị 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 kích hoạt Are Regions Enabled trong cài đặt của Studio (AltS trên Windows; S trên Mac). Hình trụ màu xanh lá cây chỉ ra khoảng cách xung quanh nhân vật của bạn mà trong đó các instance sẽ được dự đoán, và bán kính của nó sẽ tăng và giảm dựa trên các đặc tính 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 hạn máy chủ đang chạy
©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.