Bu kılavuz, sunucu yetki modeli kullanarak yüksek kaliteli, pürüzsüz çok oyunculu oyunlar oluşturmak için çeşitli teknikleri özetlemektedir.
Öngörücü örnek oluşturma (örnek dikişi)
Örnek dikişi, istemci betiklerinin RunService:BindToSimulation() geri çağırmalarında Instances'ı öngörücü şekilde oluşturmasına olanak tanır. İstemci, sunucunun yetkili kopyasının gelmesini beklemeden Instance'ı hemen oluşturur; sunucunun yetkili kopyası geldiğinde, istemci tarafından oluşturulan örnek ile sunucunun yetkili kopyası birleştirilir. Betiğinizin bakış açısına göre, Instance hemen var olur ve sunucu ile tutarlıdır.
Örnek dikişi, bir örneğin mümkün olan en kısa sürede istemcide görünür ve aktif olması gerektiği durumlarda faydalıdır. Sunucu, istemcinin ihtiyaç duyduğu herhangi bir örneği (dünyaya olan etkileriyle birlikte) bir süre sonra çoğaltacak olsa da, bu süreç sunucu iletişimi nedeniyle en az bir gidiş-geliş gecikmesi taşır. Örnekler arasında bir roketatarı ateşlemek ve fizik kısıtlamaları oluşturmak yer almaktadır — dikiş olmadan, istemci roketin kendisinden uzakta patladığını görecek veya yeni kısıtlamalar kendilerine çoğaltıldığında sallantı hissedecektir.
Teknik davranış
Örnek dikişi, istemci ve sunucuda aynı belirlenebilir GUID'yi üreterek çalışır. GUID, dört girdi üzerinden türetilir: oluşturulan Instance'ın türü, kaynağın kimliği (aşağıya bakın), mevcut simülasyon karesi ve her karede sıfırlanan bir betik çağrı sayacı.
- Instance.new() için — Kaynak kendisi betiktir (aynı metni içeren iki betik farklı kabul edilir).
- Instance:Clone() için — Her kopyalanan örnek, kaynak örneğin GUID'sini bağlam tohumu olarak kullanır.
İstemci ve sunucu girişleri konusunda hemfikir olduğunda, eşleşen GUID'ler üretir ve dikiş başarılı olur.
Uygulama
Örnek dikişini kullanmak için RunService:BindToSimulation() geri çağrısında Instance.new(), Instance:Clone() veya Instance.fromExisting() çağırın ve bu geri çağrıdan ModuleScript'te hem istemcide hem de sunucuda gerekli olan bir modül oluşturun. Diğer hiçbir şey sizin tarafınızdan gerekli değildir; sistem GUID atamasını ve uzlaşmayı otomatik olarak yönetir.
Belirli simülasyon erişimi özellikleri, örnek Name, Size veya Parent gibi bir örnek yerleştirilmeden önce serbestçe ayarlanabilir.
Simülasyon (Modül Betiği) - BindToSimulation() Geri Çağrısında Örnek Oluştur
local RunService = game:GetService("RunService")
local Simulation = {}
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local part = Instance.new("Part")
part.Name = "TahminEdilenParça"
part.Size = Vector3.new(2, 2, 2)
part.Parent = workspace -- Parça artık DataModel'da; bu noktadan sonra herhangi bir simülasyon dışı değişiklik hataya neden olur
-- Parça, istemcide hemen var olur ve sunucu ile uzlaşacaktır
end)
end
return Simulation
Instance:Clone() ve Instance.fromExisting(), kaynak örneğin hem istemciye hem de sunucuya çoğaltılması durumunda doğru dikiş yapar; her iki taraf da eşleşen kaynak GUID'sinden kopyalar ve eşleşen tahmin edilen GUID'ler üretir.
Simülasyon (Modül Betiği) - BindToSimulation() Geri Çağrısında Örneği Kopyala
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- çoğaltılan Örnek
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- Kopyalanan hiyerarşi, sunucunun yetkili kopyası ile dikişlenmiştir
end)
end
return Simulation
Konum yumuşatma
Yanlış tahmin edilen senkronize nesnelerin konumunu görsel olarak yumuşatabilirsiniz; bunun için simüle edilen nesneden farklı bir nesne render etmeniz gerekir.
- Simüle edilen nesneyi görünmez hale getirin.
- Renderer nesnesini, simüle edilen nesneyi takip eden kütlesiz, çarpışmasız, sadece görsel bir kopya olarak oluşturun.
- Renderer nesnesine, görünmez, simüle edilen nesnenin konumunu pürüzsüz bir şekilde takip eden bir betik ekleyin. Bu, render ile simülasyon arasındaki ayrımı sağlar ve görsel olarak pürüzsüz bir deneyim oluşturmak için renderer nesnesinin konumunu değiştirmeye olanak tanır.
Aşağıdaki Script örneğinde, render edilen nesne (ebeveyn) simüle edilen nesneyi pürüzsüz bir şekilde takip eder. Render edilen nesne her zaman simüle edilen nesneden biraz "geridedir", bu genellikle sorun değildir, ancak belirli durumlarda istenmeyen bir durum olabilir.
Renderer Parçası ile BasePart Konumunu Pürüzsüz Bir Şekilde Takip Et
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
-- Pürüzsüz bir şekilde takip edilecek nesne
local smoothTarget:BasePart = Workspace.SimulatedPart
-- Yumuşatılacak görsel nesne
local renderer:BasePart = script.Parent
-- Yumuşatma süresi; daha küçük, daha hızlı demektir
local smoothTime = 0.07
-- Pürüzsüz konumu hesaplamak için gereken verileri saklayın
local smoothVelocity = Vector3.new()
-- Renderer nesnesinin fiziğini devre dışı bırakın
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Hedef nesneyi pürüzsüz bir şekilde takip et
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)
Football örnek oyunu, futbol topunun konum yumuşatılmasını daha zeki bir şekilde açıp kapatmak için bu tekniğin bir varyasyonunu kullanmaktadır. Özellikle, futbol topu yalnızca simüle edilen top "bir yerden" yeterince uzakta "zıpladığında" konumunu yumuşatır. Bu yaklaşım, her iki dünyanın en iyisini sunar: futbol topu normal koşullarda görsel gecikme yaşamaz ve oyun, simüle edilen top beklenmeyen bir yere zıpladığında pozisyonunu yumuşak bir şekilde iç içe geçirir, bu muhtemelen bir ağ hatasından ya da sunucu tarafı değişikliğinden kaynaklanır.
Animasyon kodu yazma
Sunucu yetkisi altında, istemcinin simülasyonu, sunucunun bir yanlış tahmini düzeltmesi durumunda geri alınabilir ve yeniden simüle edilebilir. Geri alınma sırasında, animasyon durumu geri sarıldığından, daha önce karelerde önbelleğe aldığınız AnimationTrack işlemleri artık geçerli olmayabilir.
Animasyon mantığını yansıtma
Herhangi bir ana oyun mantığı gibi, animasyonları kontrol eden mantığın sunucu ve istemci arasında senkronize olması gerekir; aksi takdirde yanlış tahminler ve sallantılı davranışlar olabilir. RunService:BindToSimulation() aracılığıyla fonksiyonları bağlayan bir kalıp için simülasyon senkronizasyonu kısmına bakın; bu, hem istemci hem de sunucu üzerinde başlatılan bir ModuleScript içindedir.
Parça önbelleğinden kaçınma
Sunucu yetkisi olmayan betiklerde yaygın bir kalıp, AnimationTrack nesnelerini yükleme zamanında önbelleğe almak ve bunları sonsuza kadar yeniden kullanmaktır. Bu kalıp, sunucu bir yanlış tahmini düzeltip istemciyi düzeltme verileriyle geri sarıp/tekrar oynatırken bir sunucu resmi oyununda başarısız olur. Betiğiniz hala durdurulmuş veya değiştirilmiş bir parçaya referans tutuyorsa, AdjustWeight() veya AdjustSpeed() gibi çağrılar, artık görsel olarak temsil edilmeyen bir parçayı etkiler.
İstemcide Parçaları Ön Belleğe Alma (Güvenilmez)
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")
-- Animasyon parçalarını önbelleğe al
local tracks = {}
tracks["Yürümeİleri"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["Yürümeİleri"]:AdjustSpeed(1 + math.cos(time()))
end)
Parça nesnelerini tutmak yerine, animasyon kimliklerini (veya Animation örneklerini) saklayın ve etkileşimde bulunmanız gerektiğinde Animator'dan canlı parçayı sorgulayın. Bunu yapmak için iki API mevcuttur:
- Animator:GetTrackByAnimationId() — Belirli bir animasyon kimliği için mevcut olan aktif parçayı döndürür veya o kimlikle aktif animasyon yoksa nil döndürür. Bu, hangi animasyonu aradığınızı bildiğinizde kullanışlıdır.
- Animator:GetPlayingAnimationTracks() — Tüm aktif parçaları (çalma, yavaşlama veya duraklama) döndürür. Bu, tüm aktif olanları (örneğin, tüm animasyonları durdurmak veya parçaları bazı kriterlere göre bulmak için) döndürmek istediğinizde kullanışlıdır.
ModuleScript adı CustomAnimate olarak ReplicatedStorage içinde:
CustomAnimate
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Animasyon referanslarını saklayın (yüklenmiş parçalar değil)
local animations = {
Yürümeİleri = 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.Yürümeİleri)
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
Ses ve görsel efektler oynatma
Tahmin edilen bir simülasyonda, istemcinin olacağını tahmin ettiği ama sunucuda gerçekleşmeyen olaylar için efektler veya sesler tetiklenebilir. Render sistemi, yanlış tahmin edilen etkileri "geri alma" yeteneğine sahip olmalıdır. Örneğin, bir istemci bir el bombası patladı diye tahmin edebilir ve bir parçacık efekti tetikleyebilir, ancak başka bir oyuncu bombayı etkisiz hale getirdiyse, istemci parçacık efektini gizlemelidir.
Tahmin edilen bir simülasyonu görselleştirmek için, simülasyon döngüsünde bir durum makinesi kalıbını senkronize etmek ve durum değişikliklerini bir render adım işlevinde render etmek iyi bir stratejidir. Aşağıdaki örnek, bir durumu izlemek için bir el bombası simüle eden bir durum makinesi kalıbını gösterir:
El Bombası Takibi için Basit Durum Makinesi (Modül Betiği)
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)
-- Boş el bombası durumunu başlat
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- El bombası zamanlayıcısını artır
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Yanar el bombalarını patlat
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
Önceki durum makinesi mevcut olduğunda, senkronize el bombası durumuna dayalı olarak ayrı bir betikte el bombası efektlerini RunService.RenderStepped bağlantısında render edebilirsiniz:
Senkrone El Bombası Durumuna Göre Parçacıkları ve Sesleri Render Et
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- El bombası durumunu belirtmek için örneği vurgulayı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")
-- El bombası yanıyorsa parlayan parçacıkları yay
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- El bombası yeni patladıysa patlama yayımcıyı çalıştır
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
-- El bombasının durumu ve zamanı bazında vurgulama rengini değiştirin
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)
Ağ gecikmesine göre tasarım yapma
Belirli oyun mekanikleri, diğerlerinden daha iyi ağ üzerinden çok oyunculu oynamaya uygundur. Oyuncular, bir diğer oyuncunun bir eylemi gerçekleştirdiği zamandan, o oyuncunun girişini ne zaman alacaklarına kadar her zaman bir gecikme yaşarlar. Süper pürüzsüz bir çok oyunculu oyun yaratmanın en iyi yolu, oyununuzu bu sınırlamaları göz önünde bulundurarak tasarlamaktır.
Örneğin, oyuncu hareketinde daha yavaş ivmelenmeye sahip bir oyun, daha yüksek ivmelenme olan bir oyundan daha pürüzsüz görünür çünkü ağ gecikmesi nedeniyle pozisyon değişikliği, daha yüksek ivmelenme olan bir oyuna göre daha az olacaktır.
Bir başka örnek, oyuncuların bir girişi basarak anında büyük bir patlamayı tetikleyebileceği bir oyun mekaniği, girişin ardından patlamanın ertelenmesi durumuyla karşılaştırıldığında daha fazla ağ artefaktına sahip olacaktır; sanki bir fitil ateşlemek gibi. Bu, patlama etkisi yerine fitil etkisi üzerinde yeniden simülasyon yapar ki bu, daha az dikkat çekici bir ağ artefaktıdır.
Diğer oyuncu girişlerini tahmin etme
Varsayılan olarak, Roblox, her istemciden gelen girdileri diğer istemcilere yönlendirmez. Bunun oyununuz için doğru olup olmadığı, tasarımına bağlıdır:
- Temel insansal hareketler için, varsayılan davranış, diğer oyuncu karakterlerinin hareketlerinin yetkili sunucu durumundan çıkarılmadığı, bu nedenle diğer oyuncu karakterlerinin yanlış tahminlerde bulunmadığı, ancak biraz geçmişte render edileceği anlamına gelir.
- Bir yarış oyununda ise, tersine, varsayılan davranış, istemcilerin diğer oyuncuların gazı veya diğer girişleri uygulayıp uygulamadığını bilmeyeceği anlamına gelir, bu nedenle diğer arabalar yerel oyuncunun arkasında görünse de aslında önde olabilirler. Bunu hafifletmek için, oyuncu girişlerini sunucudaki niteliklerde depolayabilir ve ardından RunService:BindToSimulation() kullanarak istemci tarafında senkronize nitelikler üzerinde çalışabilirsiniz; bu, Yarış şablonundaki örnek kodda gösterilmiştir. Bu yaklaşım, birçok oyuncu girişlerinin tamamen çoğaltılmasını sağlamak için nitelikleri simülasyon girdisi olarak kullanmanıza olanak tanır.
Niteliklerde Oyuncu Girişini Depolama (Modül Betiği)
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)
-- Diğer girdileri niteliklere yaz...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Girdileri sunucudan tüm istemcilere yönlendir
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Yerel oyuncu girdilerini nitelikler olarak yaz
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Nitelikleri oyuna girdiler olarak kullan
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Gazı oyuncunun aracına uygula
end
end
end)
end)
return module
Hata ayıklama
Sunucu yetkilisi bir oyunu hata ayıklamak için kullanabileceğiniz bazı yeni araçlar ve teknikler bulunmaktadır.
Sunucu yetkisi görselleştirici
CtrlShiftF6 (Windows) veya ⌘ShiftF6 (Mac) tuşlarına basmak, Studio'nun sunucu yetkisi görselleştiricisi'ni açar ve bu, birkaç önemli bilgi parçasını gösterir:
| Detaylar | Açıklama |
|---|---|
| Örnek tahmin başarı oranı | Son 8 saniyede doğru tahmin edilen örneklerin yüzdesi. |
| Girdi kabul oranı | Tüm oyuncuların girdilerinin sunucuya zamanında ulaştığı yüzdesi. Geç kalma, bu sayıyı düşürür. |
| İstemci-sunucu adım delta | İstemci ve sunucu arasındaki kare sayısı; istemcinin katılma süresini de içerir. Bu sayının kararlılığı, sunucu ile bağlantınızın stabilitesini temsil eder. |
| RCC nabız FPS | Sunucudaki simülasyonun kare hızı. Bu sayı 59'un altına düşerse, sunucu simülasyonla başa çıkamaz ve oyun kalitesi düşer. |
| Tahmin edilen örnek sayısı | İstemcinin tahmin ettiği örnek sayısı. |
| Girdi düşme sebep sayıları | Sunucunun her sebep için bir girişi düşürdüğü zaman sayısı:
|
Simülasyon yarıçapı
Otomatik tahmine (Enum.PredictionMode.Automatic) güveniyorsanız, oyuncu karakteriniz etrafındaki tahmin yarıçapını görselleştirmek için Studio ayarlarında Bölge Oluşturma Etkin seçeneğini etkinleştirebilirsiniz (AltS Windows'ta; ⌥S Mac'te). Yeşil silindirik alan, karakteriniz etrafında örneklerin tahmin edildiği alanı gösterir ve yarıçap, cihazın performans özelliklerine göre büyüyüp küçülür.
