Questa guida delinea varie tecniche per creare giochi multiplayer di alta qualità e fluidi utilizzando il modello di autorità del server.
Creazione predittiva di istanze (instance stitching)
La creazione predittiva di istanze consente agli script client di creare in modo predittivo Instances all'interno dei callback di RunService:BindToSimulation(). Il client crea subito il Instance senza aspettare un round‑trip dal server; quando arriva la copia autorevole del server, l'istanza creata dal client e la copia autorevole del server vengono unite in una sola. Dal punto di vista del tuo script, il Instance esiste immediatamente ed è coerente con il server.
La creazione predittiva di istanze è utile nei casi in cui un'istanza deve essere visibile e attiva sul client il prima possibile. Sebbene il server alla fine replichi qualsiasi istanza di cui il client ha bisogno (insieme agli eventuali effetti che hanno avuto sul mondo), questo processo comporta almeno un round‑trip di latenza a causa della comunicazione con il server. Esempi includono l'utilizzo di un lanciatore di razzi e la creazione di vincoli fisici — senza la creazione predittiva, il client vedrà il razzo apparire lontano, oppure un certo jitter quando i nuovi vincoli vengono replicati.
Comportamento tecnico
La creazione predittiva di istanze funziona generando lo stesso GUID deterministico sia sul client che sul server. Il GUID è derivato da quattro input: il tipo di Instance che viene creato, l'identità della sorgente (vedi sotto), il frame di simulazione corrente e un contatore di chiamate per script che si resetta a ogni frame.
- Per Instance.new() — La sorgente è lo script stesso (due script con lo stesso testo sono considerati diversi).
- Per Instance.fromExisting() — La sorgente è il Instance su cui stai chiamando Instance.fromExisting().
- Per Instance:Clone() — Ogni istanza clonata utilizza il GUID dell'istanza sorgente come seme di contesto.
Se client e server concordano sugli input, producono GUID corrispondenti e la creazione predittiva ha successo.
Implementazione
Per utilizzare la creazione predittiva di istanze, chiama Instance.new(), Instance:Clone() o Instance.fromExisting() all'interno di un callback di RunService:BindToSimulation() da un ModuleScript che viene richiesto sia sul client che sul server. Non è necessario fare nient'altro da parte tua; il sistema gestisce automaticamente l'assegnazione e la riconciliazione del GUID.
Puoi impostare liberamente proprietà non di accesso alla simulazione come Name, Size o Parent su un'istanza prima che venga parentata nel DataModel.
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 -- La parte è ora nel modello di dati; eventuali modifiche di accesso non di simulazione genereranno un errore dopo questo
-- La parte esiste immediatamente sul client e sarà riconciliata con il server
end)
end
return SimulationInstance:Clone() e Instance.fromExisting() funzionano correttamente quando l'istanza sorgente è stata replicata sia sul client che sul server; entrambi i lati clonano da GUID sorgenti corrispondenti e producono GUID predittivi corrispondenti.
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- un'istanza replicata
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- Gerarchia clonata è incollata con la copia autorevole del server
end)
end
return SimulationSmussamento delle posizioni
Puoi rendere visivamente più fluida la posizione di oggetti sincronizzati mal predetti rendendo un oggetto diverso da quello che viene simulato.
- Rendi invisibile l'oggetto simulato.
- Crea un oggetto renderer come un clone non collisibile, senza massa e solo visivo per tracciare l'oggetto simulato.
- Attacca uno script all'oggetto renderer che tracci in modo fluido la posizione dell'oggetto invisibile e simulato. Questa separazione tra rendering e simulazione ti consente di alterare la posizione dell'oggetto renderer per creare un'esperienza visivamente fluida.
Nel seguente esempio di Script, l'oggetto reso (genitore) traccia fluidamente l'oggetto simulato. L'oggetto reso è sempre leggermente "indietro" rispetto all'oggetto simulato, il che di solito va bene ma può essere indesiderabile in certe situazioni.
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- Oggetto da seguire fluidamente
local smoothTarget:BasePart = workspace.SimulatedPart
-- Oggetto visivo che sarà reso in modo fluido
local renderer:BasePart = script.Parent
-- Tempo per smussare; più piccolo significa più veloce
local smoothTime = 0.07
-- Memorizza i dati necessari per calcolare la posizione fluida
local smoothVelocity = Vector3.new()
-- Disabilita la fisica dell'oggetto renderer
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Segui fluidamente l'oggetto target
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)Il gioco di esempio Soccer utilizza una variazione di questa tecnica per attivare e disattivare in modo più intelligente il smussamento della posizione per la palla da calcio. In particolare, la palla da calcio smussa la sua posizione solo quando la palla simulata è "saltata" abbastanza lontano dalla palla resa. Questo approccio fornisce il meglio di entrambi i mondi: la palla da calcio non ha latenza visiva in condizioni normali, e il gioco smussa la sua posizione solo dopo che la palla simulata è saltata inaspettatamente in una nuova posizione, probabilmente a causa di un artefatto di rete o di una modifica lato server.
Scrivere codice di animazione
Sotto l'autorità del server, la simulazione del client può essere riavviata e risimulata quando il server corregge una malpredizione. Durante il riavvio, lo stato di animazione viene riportato indietro, il che significa che AnimationTrack gestisce che hai memorizzato in frame precedenti potrebbe non essere più valido.
Logica di animazione a specchio
Come per qualsiasi logica di gioco core, la logica per controllare le animazioni deve essere sincronizzata tra server e client o potrebbero esserci malpredizioni e comportamenti tremolanti. Vedi sincronizzazione delle simulazioni per un modello che collega le funzioni tramite RunService:BindToSimulation() in un ModuleScript che viene inizializzato sia sul client che sul server.
Evita la memorizzazione delle tracce
Un modello comune negli script non autoritari del server è memorizzare gli oggetti AnimationTrack al momento del caricamento e riutilizzarli indefinitamente. Questo modello fallisce in un gioco sotto autorità del server quando il server corregge una malpredizione e il client riavvolge/riproduce la sua simulazione con dati corretti. Se il tuo script mantiene ancora un riferimento a una traccia bloccata o sostituita, chiamate come AdjustWeight() o AdjustSpeed() opereranno su una traccia che non è più visivamente rappresentata.
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")
-- Memorizza tracce di animazione
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)Invece di mantenere oggetti traccia, memorizza i ID delle animazioni (o istanze di Animation) e interroga il Animator per la traccia attiva ogni volta che hai bisogno di interagire con essa. Sono disponibili due API per questo:
- Animator:GetTrackByAnimationId() — Restituisce la traccia attualmente attiva per un ID di animazione specifico, o nil se non ci sono animazioni attive con quell'ID. Usa questo quando sai quale animazione specifica stai cercando.
- Animator:GetPlayingAnimationTracks() — Restituisce tutte le tracce attive (in riproduzione, in dissolvenza o in pausa). Usa questo quando devi iterare su tutto ciò che è attivo (ad esempio, per fermare tutte le animazioni o trovare tracce in base a qualche criterio).
ModuleScript chiamato CustomAnimate in ReplicatedStorage:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Memorizza i riferimenti alle animazioni (tracce non caricate)
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 CustomAnimateRiproduzione di suoni e effetti visivi
In una simulazione predittiva, è possibile attivare effetti o suoni per eventi che il client prevedeva che sarebbero accaduti ma che in realtà non si sono verificati sul server. Il sistema di rendering dovrebbe essere pronto a "annullare" eventuali effetti mal predetti. Ad esempio, un client potrebbe prevedere che una granata sia esplosa e attivare un effetto di particelle, ma se un altro giocatore ha disinnescato la granata, il client dovrebbe nascondere l'effetto di particelle.
Una buona strategia per il rendering di una simulazione predittiva è sincronizzare un modello a macchina a stati all'interno del ciclo di simulazione e rendere le modifiche allo stato in una funzione di passo di rendering. Il seguente esempio simula una granata con un modello a macchina a stati:
local module = {}
module.GrenadeStates = {
Idle = 0,
Lit = 1,
Exploded = 2,
Defused = 3,
}
module.GrenadeExplodeTime = 3.0
module.Initialize = function(grenade)
RunService:BindToSimulation(function(deltaTime)
-- Inizializza lo stato della granata vuoto
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Incrementa il timer della granata
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Esplodi le granate accese
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 moduleCon la macchina a stati precedente in atto, puoi rendere gli effetti della granata in una connessione RunService.RenderStepped all'interno di uno script separato basato sullo stato della granata sincronizzato:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Evidenzia l'istanza per indicare lo stato della granata
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")
-- Emetti le particelle accese se la granata è accesa
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Riproduci l'emettitore di esplosione se la granata è appena esplosa
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
-- Cambia il colore dell'evidenziazione della granata in base allo stato e al tempo
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)Progettare attorno alla latenza di rete
Alcune meccaniche di gioco si prestano meglio al multiplayer in rete rispetto ad altre meccaniche. I giocatori avranno sempre un certo ritardo tra il momento in cui un altro giocatore compie un'azione e il momento in cui ricevono l'input di quel giocatore. Il modo migliore per creare un gioco multiplayer super fluido è progettare il tuo gioco tenendo conto di queste limitazioni.
Ad esempio, un gioco con una accelerazione più lenta nel movimento del giocatore apparirà più fluido rispetto a uno con una maggiore accelerazione, perché la differenza di posizione causata dalla latenza di rete dell'input del giocatore sarà minore rispetto a un gioco con una maggiore accelerazione.
Come altro esempio, una meccanica di gioco in cui i giocatori possono attivare immediatamente una grande esplosione premendo un input avrà più artefatti di rete rispetto a se l'esplosione fosse ritardata dopo l'input, come se si stesse accendendo una miccia. Questo mette la risimulazione sull'effetto della miccia invece che sull'effetto dell'esplosione, il quale è un artefatto di rete meno percettibile.
Prevedere gli input di altri giocatori
Per impostazione predefinita, Roblox non inoltra gli input da ciascun client a ogni altro client. Se questo sia adatto per il tuo gioco dipende dal suo design:
- Per il movimento umano di base, il comportamento predefinito significa che i movimenti degli altri personaggi giocatori non vengono estrapolati dallo stato autorevole del server e, di conseguenza, gli altri personaggi giocatori non malprediranno ma verranno resi leggermente nel passato.
- In un gioco di corse, al contrario, il comportamento predefinito significa che i client non sapranno se altri giocatori stanno applicando l'accelerazione o altri input, quindi altre auto potrebbero apparire dietro il giocatore locale anche se in realtà sono avanti. Per attenuare questo, puoi memorizzare gli input dei giocatori in attributi sul server e operare su quegli attributi sincronizzati lato client utilizzando RunService:BindToSimulation() come dimostrato nel seguente campione di codice e nel template Racing. Questo approccio ti consente di utilizzare gli attributi come input per la tua simulazione per avere inputs completamente replicati dai giocatori.
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)
-- Scrivi eventuali altri input negli attributi...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Inoltra inputs dal server a tutti i 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
-- Scrivi gli input del giocatore locale come attributi
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Usa gli attributi come input per il gioco
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Applica l'accelerazione al veicolo del giocatore
end
end
end)
end)
return moduleDebugging
Ci sono alcuni nuovi strumenti e tecniche che puoi utilizzare per eseguire il debug di un gioco sotto autorità del server.
Visualizzatore di autorità del server
Premere CtrlShiftF6 (Windows) o ⌘ShiftF6 (Mac) apre il visualizzatore di autorità del server di Studio che mostra diversi elementi chiave di informazione:
| Dettagli | Descrizione |
|---|---|
| Tasso di successo della previsione delle istanze | La percentuale di istanze previste correttamente negli ultimi 8 secondi. |
| Tasso di accettazione degli input | La percentuale di tutti gli input dei giocatori che sono arrivati in tempo sul server. Inputs in ritardo abbasseranno questo numero. |
| Delta dei passi client-server | Il numero di frame tra il client e il server, incluso il tempo di ingresso del client. La stabilità di questo numero rappresenta la stabilità della tua connessione al server. |
| FPS del battito cardiaco RCC | Il frame rate della simulazione sul server. Se questo numero scende sotto 59, il server non riesce a tenere il passo con la simulazione e la qualità del gioco degraderà. |
| Conteggio delle istanze previste | Il numero di istanze che il tuo client sta prevedendo. |
| Conteggi dei motivi di caduta degli input | Il numero di volte in cui il server ha eliminato un input per ciascun motivo:
|
Raggio di simulazione
Quando ci si basa sulla previsione automatica (Enum.PredictionMode.Automatic), puoi visualizzare il raggio di previsione attorno al tuo personaggio giocatore abilitando Are Regions Enabled nelle impostazioni di Studio (AltS su Windows; ⌥S su Mac). Il cilindro verde indica l'intervallo attorno al tuo personaggio nel quale le istanze vengono previste, e il suo raggio cresce e si restringe in base alle caratteristiche di prestazione del dispositivo.
