Teknik otoritas server

*Konten ini diterjemahkan menggunakan AI (Beta) dan mungkin mengandung kesalahan. Untuk melihat halaman ini dalam bahasa Inggris, klik di sini.

Panduan ini menjelaskan berbagai teknik untuk membuat permainan multiplayer berkualitas tinggi dan lancar menggunakan model otoritas server.

Pembuatan instance prediktif (penyambungan instance)

Penyambungan instance memungkinkan skrip klien secara prediktif membuat Instances di dalam callback RunService:BindToSimulation(). Klien membuat Instance segera tanpa menunggu perjalanan pulang-pergi server; ketika salinan otoritatif server tiba, instance yang dibuat klien dan salinan otoritatif server disatukan menjadi satu. Dari perspektif skrip Anda, Instance ada segera dan konsisten dengan server.

Penyambungan instance berguna dalam kasus di mana sebuah instance harus terlihat dan aktif di klien secepat mungkin. Meskipun server akhirnya akan mereplikasi instance apa pun yang dibutuhkan klien (bersama dengan efek apa pun yang dimilikinya di dunia), proses ini memerlukan setidaknya satu perjalanan pulang-pergi latensi karena komunikasi server. Contoh termasuk penembakan peluncur roket dan pembuatan batas fisika — tanpa penyambungan, klien akan melihat roket muncul dari jauh, atau beberapa goyangan saat batas baru direplikasi kepada mereka.

Perilaku teknis

Penyambungan instance bekerja dengan menghasilkan GUID yang sama secara deterministik di klien dan server. GUID diturunkan dari empat input: jenis Instance yang dibuat, identitas sumber (lihat di bawah), frame simulasi saat ini, dan penghitung panggilan per-skrip yang direset setiap frame.

Jika klien dan server sepakat pada input, mereka menghasilkan GUID yang cocok dan penyambungan berhasil.

Implementasi

Untuk memanfaatkan penyambungan instance, panggil Instance.new(), Instance:Clone(), atau Instance.fromExisting() di dalam callback BindToSimulation() dari ModuleScript yang diperlukan di klien dan di server. Tidak ada yang diperlukan dari pihak Anda; sistem menangani penugasan GUID dan rekonsiliasi secara otomatis.

Anda dapat dengan bebas mengatur properti non-akses simulasi seperti Name, Size, atau Parent pada sebuah instance sebelum diparent ke dalam DataModel.

Simulasi (ModuleScript) - Buat Instance dalam 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 -- Bagian sekarang ada di model data; perubahan akses non-simulasi akan menghasilkan kesalahan setelah ini
-- Bagian ada segera di klien dan akan disesuaikan dengan server
end)
end
return Simulation

Instance:Clone() dan Instance.fromExisting() menyambung dengan benar ketika instance sumber telah direplikasi ke klien dan server; kedua sisi mengkloning dari GUID sumber yang cocok dan menghasilkan GUID prediksi yang cocok.

Simulasi (ModuleScript) - Klon Instance dalam Callback BindToSimulation()
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- sebuah Instance yang direplikasi
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- Hierarki yang diklon adalah disambungkan dengan salinan otoritatif server
end)
end
return Simulation

Perataan posisi

Anda dapat memperhalus posisi objek terkoordinasi yang salah prediksi secara visual dengan merender objek yang berbeda dari yang sedang disimulasi.

  1. Buat objek simulasi tidak terlihat.
  2. Buat objek renderer sebagai tiruan tanpa massa, tidak dapat bertabrakan, dan hanya visual untuk melacak objek simulasi.
  3. Lampirkan skrip ke objek renderer yang secara halus melacak posisi objek simulasi yang tidak terlihat. Pemisahan antara rendering dan simulasi memungkinkan Anda mengubah posisi objek renderer untuk menciptakan pengalaman yang visual lebih halus.

Dalam contoh Script berikut, objek yang dirender (induk) secara halus melacak objek simulasi. Objek yang dirender selalu sedikit "tertinggal" di belakang objek simulasi yang biasanya baik tetapi mungkin tidak diinginkan dalam situasi tertentu.

Lacak Posisi BasePart Secara Halus dengan Renderer Part
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- Objek untuk dilacak secara halus
local smoothTarget:BasePart = workspace.SimulatedPart
-- Objek visual yang akan diperlunak
local renderer:BasePart = script.Parent
-- Waktu untuk memperhalus; semakin kecil berarti semakin cepat
local smoothTime = 0.07
-- Simpan data yang diperlukan untuk menghitung posisi halus
local smoothVelocity = Vector3.new()
-- Nonaktifkan fisika objek renderer
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Lacak objek target secara halus
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

Permainan contoh Sepakbola menggunakan variasi teknik ini untuk lebih cerdas menghidupkan dan mematikan perataan posisi untuk bola sepak. Secara khusus, bola sepak hanya memperhalus posisinya ketika bola simulasi telah "melompat" cukup jauh dari bola yang dirender. Pendekatan ini memberikan yang terbaik dari kedua dunia: bola sepak tidak memiliki latensi visual dalam kondisi normal, dan permainan secara halus menginterpolasi posisinya hanya setelah bola simulasi tiba-tiba melompat ke lokasi baru, kemungkinan besar karena artefak jaringan atau perubahan di sisi server.

Di bawah otoritas server, simulasi klien dapat direlokasi dan disimulasi ulang ketika server mengoreksi kesalahan prediksi. Selama relokasi, status animasi terputar, yang berarti AnimationTrack yang Anda cache di frame sebelumnya mungkin tidak lagi valid.

Logika animasi cermin

Seperti halnya logika inti permainan, logika untuk mengontrol animasi harus sinkron antara server dan klien atau mungkin ada kesalahan prediksi dan perilaku goyang. Lihat sinkronisasi simulasi untuk pola yang mengikat fungsi melalui RunService:BindToSimulation() dalam ModuleScript yang diinisialisasi di klien dan server.

Hindari caching track

Pola umum dalam skrip non-otoritas server adalah menyimpan objek AnimationTrack saat waktu pemuatan dan menggunakannya kembali tanpa batas. Pola ini gagal dalam permainan dengan otoritas server ketika server mengoreksi kesalahan prediksi dan klien memutar ulang/simulasi ulang dengan data yang dikoreksi. Jika skrip Anda masih memegang referensi ke jalur yang dihentikan atau diganti, panggilan seperti AdjustWeight() atau AdjustSpeed() akan beroperasi pada jalur yang tidak lagi direpresentasikan secara visual.

Cache Tracks di Klien (Tidak Dapat Diandalkan)
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")
-- Cache jalur animasi
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

Alih-alih memegang objek track, simpan ID animasi (atau instance Animation) dan query Animator untuk track yang hidup setiap kali Anda perlu berinteraksi dengannya. Dua API tersedia untuk ini:

  • Animator:GetTrackByAnimationId() — Mengembalikan track yang sedang aktif untuk ID animasi tertentu, atau nil jika tidak ada animasi aktif dengan ID tersebut. Gunakan ini ketika Anda tahu animasi spesifik mana yang Anda cari.
  • Animator:GetPlayingAnimationTracks() — Mengembalikan semua track aktif (bermain, memudar, atau terjeda). Gunakan ini ketika Anda perlu mengiterasi semua yang aktif (misalnya, untuk menghentikan semua animasi atau menemukan track berdasarkan beberapa kriteria).

ModuleScript bernama CustomAnimate di ReplicatedStorage:

CustomAnimate
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Simpan referensi animasi (bukan track yang dimuat)
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

Memutar suara dan efek visual

Dalam simulasi yang diprediksi, dimungkinkan untuk memicu efek atau suara untuk peristiwa yang diperkirakan klien akan terjadi tetapi yang tidak pernah terjadi di server. Sistem rendering harus bersiap untuk "menghapus" efek yang salah prediksi. Misalnya, klien mungkin memprediksi bahwa granat meledak dan memicu efek partikel, tetapi jika pemain lain menjinakkan granat tersebut, klien harus menyembunyikan efek partikel tersebut.

Strategi yang baik untuk merender simulasi yang diprediksi adalah dengan menyinkronkan pola mesin status dalam loop simulasi dan merender perubahan ke status dalam fungsi langkah render. Contoh berikut mensimulasikan granat dengan pola mesin status:

Pola Mesin Status Sederhana untuk Melacak Granat (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)
-- Inisialisasi status granat yang kosong
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Increment timer granat
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Meledakkan granat yang menyala
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

Dengan mesin status sebelumnya di tempat, Anda dapat merender efek granat dalam koneksi RunService.RenderStepped dalam skrip terpisah berdasarkan status granat yang disinkronkan:

Render Partikel dan Suara berdasarkan Status Granat yang Disinkronkan
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Sorot instance untuk menunjukkan status granat
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")
-- Emit partikel menyala jika granat menyala
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Putar pemancar ledakan jika granat baru saja meledak
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
-- Ubah warna sorotan granat berdasarkan status dan waktu
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)

Mendesain di sekitar latensi jaringan

Beberapa mekanik permainan lebih cocok untuk multiplayer jaringan daripada mekanik lainnya. Pemain akan selalu memiliki sedikit penundaan antara saat pemain lain melakukan tindakan dan saat mereka menerima input dari pemain tersebut. Cara terbaik untuk membuat permainan multiplayer yang sangat lancar adalah dengan merancang permainan Anda dengan batasan ini diingat.

Misalnya, permainan dengan percepatan yang lebih lambat pada gerakan pemain akan tampil lebih halus daripada yang memiliki percepatan lebih tinggi karena perbedaan posisi yang disebabkan oleh latensi jaringan dari input pemain akan lebih sedikit daripada di permainan dengan percepatan lebih tinggi.

Sebagai contoh lain, mekanik permainan di mana pemain dapat seketika memicu ledakan besar dengan menekan input akan memiliki lebih banyak artefak jaringan daripada jika ledakan tersebut ditunda setelah input, seakan-akan dengan menyalakan sebuah sumbu. Ini memindahkan simulasi ulang pada efek sumbu daripada efek ledakan yang merupakan artefak jaringan yang kurang terlihat.

Memprediksi input pemain lain

Secara default, Roblox tidak meneruskan input dari setiap klien ke setiap klien lainnya. Apakah ini tepat untuk permainan Anda tergantung pada desainnya:

  • Untuk gerakan humanoid dasar, perilaku default berarti bahwa gerakan karakter pemain lain tidak diekstrapolasi dari status server yang otoritatif dan, sebagai hasilnya, karakter pemain lain tidak akan salah memprediksi tetapi akan dirender sedikit di masa lalu.
  • Dalam permainan balap, sebaliknya, perilaku default berarti bahwa klien tidak akan tahu apakah pemain lain sedang menerapkan throttle atau input lainnya, sehingga mobil lain dapat muncul di belakang pemain lokal bahkan jika mereka sebenarnya di depan. Untuk mengatasi hal ini, Anda dapat menyimpan input pemain di atribut di server dan beroperasi pada atribut yang disinkronkan tersebut di sisi klien menggunakan RunService:BindToSimulation() seperti yang ditunjukkan dalam contoh kode berikut dan template Balap. Pendekatan ini memungkinkan Anda menggunakan atribut sebagai input untuk simulasi Anda agar memiliki input pemain yang sepenuhnya direplikasi.
Menyimpan Input Pemain dalam Atribut (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)
-- Tulis input lainnya ke dalam atribut...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Meneruskan input dari server ke semua klien
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Tulis input pemain lokal sebagai atribut
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Gunakan atribut sebagai input untuk permainan
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Terapkan throttle ke kendaraan pemain
end
end
end)
end
return module

Debugging

Ada beberapa alat dan teknik baru yang dapat Anda gunakan untuk mendebug permainan yang otoritatif server.

Visualizer otoritas server

Menekan CtrlShiftF6 (Windows) atau ShiftF6 (Mac) membuka visualizer otoritas server Studio yang menunjukkan beberapa potongan informasi kunci:

DetailDeskripsi
Tingkat keberhasilan prediksi instancePersentase instance yang diprediksi dengan benar selama 8 detik terakhir.
Tingkat penerimaan inputPersentase dari semua input pemain yang tiba tepat waktu di server. Input yang terlambat akan menurunkan angka ini.
Delta langkah klien-serverJumlah frame antara klien dan server, termasuk waktu bergabung klien. Stabilitas angka ini mencerminkan stabilitas koneksi Anda ke server.
FPS heartbeat RCCFrame rate simulasi di server. Jika angka ini turun di bawah 59, server tidak dapat mengikuti simulasi dan kualitas permainan akan menurun.
Jumlah instance yang diprediksiJumlah instance yang diprediksi klien Anda prediksi.
Jumlah alasan penurunan input

Jumlah kali server telah menurunkan input untuk masing-masing alasan:

  • [x] terlalu tua — Input tiba terlambat, berarti jaringan Anda memburuk atau klien tidak dapat mengikuti simulasi.
  • [x] di luar urutan — Terjadi bug jaringan yang menyebabkan input Anda disusun ulang dan dibuang.
  • [x] buffer penuh — Server tidak dapat menyimpan input Anda. Entah jaringan Anda tiba-tiba membaik atau server tidak dapat mengikuti simulasi.

Radius simulasi

Ketika bergantung pada prediksi otomatis (Enum.PredictionMode.Automatic), Anda dapat memvisualisasikan radius prediksi di sekitar karakter pemain Anda dengan mengaktifkan Apakah Wilayah Diaktifkan di pengaturan Studio (AltS pada Windows; S pada Mac). Silinder hijau menunjukkan kisaran di sekitar karakter Anda di mana instance diprediksi, dan radiusnya tumbuh dan menyusut berdasarkan karakteristik kinerja perangkat.

Radius simulasi di sekitar karakter pemain dengan otoritas server yang berjalan
©2026 Roblox Corporation. Roblox, logo Roblox, dan Powering Imagination termasuk dalam merek dagang kami yang terdaftar dan tidak terdaftar di AS dan negara lainnya.