Dieser Leitfaden skizziert verschiedene Techniken zur Erstellung hochwertiger, reibungsloser Mehrspielerspiele unter Verwendung des Serverautorisierungsmodells.
Vorhersagende Instanzerstellung (Instance Stitching)
Instance Stitching ermöglicht es Clientskripten, Instances innerhalb von RunService:BindToSimulation()-Rückrufen vorhersagend zu erstellen. Der Client erstellt die Instance sofort, ohne auf eine Server-Rundreise zu warten; wenn die autoritative Kopie des Servers ankommt, werden die vom Client erstellte Instanz und die autoritative Kopie des Servers zu einer verschmolzen. Aus der Perspektive Ihres Skripts existiert die Instance sofort und ist konsistent mit dem Server.
Instance Stitching ist nützlich in Fällen, in denen eine Instanz so schnell wie möglich sichtbar und aktiv auf dem Client sein muss. Während der Server schließlich jede Instanz, die der Client benötigt (neben allen Effekten, die sie auf die Welt hatten), replizieren wird, verursacht dieser Prozess mindestens eine Rundreise von Latenz aufgrund der Serverkommunikation. Beispiele sind das Abfeuern eines Raketenwerfers und das Erstellen physikalischer Beschränkungen — ohne Stitching wird der Client sehen, wie die Rakete weit entfernt erscheint, oder es gibt Ruckler, wenn die neuen Beschränkungen zu ihm repliziert werden.
Technisches Verhalten
Instance Stitching funktioniert, indem sowohl der Client als auch der Server die gleiche deterministische GUID generieren. Die GUID wird aus vier Eingaben abgeleitet: dem Typ der zu erstellenden Instance, der Identität der Quelle (siehe unten), dem aktuellen Simulationsrahmen und einem pro-Skript-Aufrufzähler, der bei jedem Rahmen zurückgesetzt wird.
- Für Instance.new() — Die Quelle ist das Skript selbst (zwei Skripte mit demselben Text werden als unterschiedlich betrachtet).
- Für Instance.fromExisting() — Die Quelle ist die Instance, auf der Sie Instance.fromExisting() aufrufen.
- Für Instance:Clone() — Jede geklonte Instanz verwendet die GUID der Quellinstanz als Kontext-Seed.
Wenn Client und Server sich über die Eingaben einig sind, erzeugen sie übereinstimmende GUIDs und das Stitching gelingt.
Implementierung
Um Instance Stitching zu nutzen, rufen Sie Instance.new(), Instance:Clone() oder Instance.fromExisting() innerhalb eines BindToSimulation() Rückrufs von einem ModuleScript auf, das sowohl auf dem Client als auch auf dem Server benötigt wird. Nichts anderes ist von Ihrer Seite erforderlich; das System erledigt die GUID-Zuweisung und die Rekonsolidierung automatisch.
Sie können auf einer Instanz ohne weitere Simulationszugriff-Eigenschaften wie Name, Size oder Parent setzen, bevor sie in das DataModel eingefügt wird.
Simulation (ModuleScript) - Instanz in einem BindToSimulation() Rückruf erstellen
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 -- Teil ist jetzt im DataModel; alle Änderungen an den Eigenschaften der Nicht-Simulation verursachen einen Fehler nach diesem
-- Teil existiert sofort auf dem Client und wird mit dem Server rekonsolidiert
end)
end
return Simulation
Instance:Clone() und Instance.fromExisting() arbeiten korrekt, wenn die Quellinstanz sowohl an den Client als auch an den Server repliziert wurde; beide Seiten klonen von übereinstimmenden Quell-GUIDs und erzeugen übereinstimmende vorhergesagte GUIDs.
Simulation (ModuleScript) - Instanz in einem BindToSimulation() Rückruf klonen
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- eine replizierte Instanz
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- Die geklonte Hierarchie wird mit der autoritativen Kopie des Servers abgestimmt
end)
end
return Simulation
Positionsglättung
Sie können die Position von fehlvorhergesagten synchronisierten Objekten visuell glätten, indem Sie ein anderes Objekt rendern als das, was simuliert wird.
- Machen Sie das simulierte Objekt unsichtbar.
- Erstellen Sie ein Renderer-Objekt als masseloses, nicht kollidierendes, visuell nur Klon, um das simulierte Objekt zu verfolgen.
- Fügen Sie dem Renderer-Objekt ein Skript hinzu, das die Position des unsichtbaren, simulierten Objekts glatt verfolgt. Diese Trennung zwischen Rendering und Simulation ermöglicht es Ihnen, die Position des Renderer-Objekts zu ändern, um eine visuell glatte Erfahrung zu schaffen.
Im folgenden Beispiel Script verfolgt das gerenderte Objekt (Elternteil) das simulierte Objekt sanft. Das gerenderte Objekt ist immer leicht "hinter" dem simulierten Objekt, was normalerweise in Ordnung, aber in bestimmten Situationen unerwünscht sein kann.
Die Position von BasePart sanft mit Renderer-Part verfolgen
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
-- Objekt, das sanft verfolgt werden soll
local smoothTarget:BasePart = Workspace.SimulatedPart
-- Visuelles Objekt, das geglättet wird
local renderer:BasePart = script.Parent
-- Zeit, um zu glätten; kleiner bedeutet schneller
local smoothTime = 0.07
-- Speichern von Daten, die benötigt werden, um die glatte Position zu berechnen
local smoothVelocity = Vector3.new()
-- Deaktivieren Sie die Physik des Renderer-Objekts
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Sanft das Zielobjekt verfolgen
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)
Das Beispielspiel Soccer verwendet eine Variation dieser Technik, um die Positionsglättung für den Fußball intelligenter ein- und auszuschalten. Konkret glättet der Fußball seine Position nur, wenn der simulierte Ball weit genug von dem gerenderten Ball "gesprungen" ist. Dieser Ansatz bietet das Beste aus beiden Welten: Der Fußball hat unter normalen Bedingungen keine visuelle Latenz, und das Spiel interpoliert seine Position nur, nachdem der simulierte Ball unerwartet an einen neuen Standort gesprungen ist, wahrscheinlich aufgrund eines Netzwerkartefakts oder einer serverseitigen Änderung.
Animationscode schreiben
Unter Serverautorisierung kann die Simulation des Clients zurückgesetzt und neu simuliert werden, wenn der Server eine Fehlvorhersage korrigiert. Während des Rollbacks wird der Animationsstatus zurückgespult, was bedeutet, dass AnimationTrack, die Sie in früheren Frames zwischengespeichert haben, möglicherweise nicht mehr gültig ist.
Animationslogik spiegeln
Wie bei jeder Kernspielmechanik muss die Logik zur Steuerung der Animationen zwischen Server und Client synchron sein, da sonst Fehlvorhersagen und ruckelige Verhaltensweisen auftreten können. Siehe Simulationssynchronisation für ein Muster, das Funktionen über RunService:BindToSimulation() in einem ModuleScript bindet, das sowohl auf dem Client als auch auf dem Server initialisiert wird.
Vermeiden Sie das Zwischenspeichern von Tracks
Ein gängiges Muster in Skripten ohne Serverautorisierung besteht darin, AnimationTrack-Objekte zur Ladezeit zwischenzuspeichern und unbegrenzt wiederzuverwenden. Dieses Muster schlägt in einem serverautorisierenden Spiel fehl, wenn der Server eine Fehlvorhersage korrigiert und der Client seine Simulation mit korrigierten Daten zurückspult/wiederholt. Wenn Ihr Skript immer noch einen Verweis auf einen gestoppten oder ersetzten Track hält, werden Aufrufe wie AdjustWeight() oder AdjustSpeed() auf einem Track ausgeführt, der nicht mehr visuell dargestellt wird.
Tracks auf dem Client zwischenspeichern (unzuverlässig)
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")
-- Animations-Tracks zwischenspeichern
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)
Anstatt auf Track-Objekten zu bestehen, speichern Sie die Animations-IDs (oder Animation-Instanzen) und fragen Sie das Animator nach dem aktiven Track, wann immer Sie damit interagieren müssen. Zwei APIs stehen dafür zur Verfügung:
- Animator:GetTrackByAnimationId() — Gibt den derzeit aktiven Track für eine bestimmte Animations-ID zurück oder nil, wenn es keine aktiven Animationen mit dieser ID gibt. Verwenden Sie dies, wenn Sie wissen, welche spezifische Animation Sie suchen.
- Animator:GetPlayingAnimationTracks() — Gibt alle aktiven Tracks (spielend, ausblendend oder pausiert) zurück. Verwenden Sie dies, wenn Sie über alles Aktive iterieren müssen (zum Beispiel um alle Animationen zu stoppen oder Tracks nach einem Kriterium zu finden).
ModuleScript namens CustomAnimate in ReplicatedStorage:
CustomAnimate
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Animationen-Referenzen speichern (nicht geladene Tracks)
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
Sounds und visuelle Effekte abspielen
In einer vorhergesagten Simulation ist es möglich, Effekte oder Sounds für Ereignisse zu triggern, von denen der Client vorhergesagt hat, dass sie eintreten würden, die aber auf dem Server nicht aufgetreten sind. Das Rendering-System sollte bereit sein, falsche Effekte "rückgängig zu machen". Zum Beispiel könnte ein Client vorhersagen, dass eine Granate explodiert ist und einen Partikeleffekt auslösen, aber wenn ein anderer Spieler die Granate entschärft hat, sollte der Client den Partikeleffekt verbergen.
Eine gute Strategie für das Rendern einer vorhergesagten Simulation besteht darin, ein Zustandsmaschinenmuster innerhalb der Simulationsschleife zu synchronisieren und Änderungen am Zustand in einer Render-Schritt-Funktion darzustellen. Das folgende Beispiel simuliert eine Granate mit einem Zustandsmaschinenmuster:
Einfaches Zustandsmaschinenmuster zum Verfolgen einer Granate (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)
-- Leeren Granatenstatus initialisieren
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Granaten-Timer erhöhen
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Explodieren bei entzündeten Granaten
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
Mit der vorherigen Zustandsmaschine können Sie Granateneffekte in einer RunService.RenderStepped-Verbindung innerhalb eines separaten Skripts basierend auf dem synchronisierten Granatenstatus rendern:
Partikel und Geräusche basierend auf dem synchronisierten Granatenstatus rendern
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Hervorhebungsinstanz zur Indikation des Granaten状态
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 the lit particles if the grenade is lit
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Play the explosion emitter if the grenade just exploded
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
-- Change the grenade's highlight color based on the state and time
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)
Designing around network latency
Einige Spielmechaniken eignen sich besser für netzwerkbasierte Multiplayer als andere Mechaniken. Spieler werden immer eine gewisse Verzögerung haben zwischen dem Zeitpunkt, an dem ein anderer Spieler eine Aktion ausführt, und dem Zeitpunkt, an dem sie die Eingabe dieses Spielers erhalten. Der beste Weg, um ein super flüssiges Multiplayer-Spiel zu erstellen, besteht darin, Ihr Spiel mit diesen Einschränkungen im Hinterkopf zu gestalten.
Zum Beispiel wird ein Spiel mit langsamerer Beschleunigung bei der Spielerbewegung glatter erscheinen als eines mit höherer Beschleunigung, da der Unterschied in der Position, der durch die Netzwerkverzögerung der Spielereingabe verursacht wird, geringer sein wird als in einem Spiel mit höherer Beschleunigung.
Als weiteres Beispiel wird eine Spielmechanik, bei der Spieler durch Drücken einer Eingabe sofort eine große Explosion auslösen können, mehr Netzwerkartefakte aufweisen als wenn die Explosion nach der Eingabe verzögert ist, als ob man eine Zündschnur entzündet. Dies versetzt die Neusimulation auf den Zündschnureffekt statt auf den Explosionseffekt, was ein weniger auffälliges Netzwerkartefakt ist.
Vorhersage anderer Spielereingaben
Standardmäßig leitet Roblox die Eingaben von jedem Client nicht an jeden anderen Client weiter. Ob dies für Ihr Spiel richtig ist, hängt von dessen Design ab:
- Bei grundlegender humanoider Bewegung bedeutet das Standardverhalten, dass die Bewegungen anderer Spielercharaktere nicht aus dem autoritativen Serverstatus extrapoliert werden und als Ergebnis werden andere Spielercharaktere nicht fehlvorhergesagt, sondern werden leicht in der Vergangenheit gerendert.
- In einem Rennspiel bedeutet das Standardverhalten hingegen, dass die Clients nicht wissen, ob andere Spieler das Gaspedal oder andere Eingaben betätigen, sodass andere Autos hinter dem lokalen Spieler erscheinen können, auch wenn sie tatsächlich voraus sind. Um dem entgegenzuwirken, können Sie die Spielerinputs in Attributen auf dem Server speichern und mit diesen synchronisierten Attributen clientseitig unter Verwendung von RunService:BindToSimulation() arbeiten, wie im folgenden Codebeispiel und der Racing Vorlage demonstriert. Dieser Ansatz ermöglicht es Ihnen, Attribute als Eingaben für Ihre Simulation zu verwenden, um vollständig replizierte Spielereingaben zu haben.
Speichern von Spielereingaben in Attributen (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)
-- Schreiben Sie andere Eingaben in Attribute...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Eingaben vom Server an alle Clients weiterleiten
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Lokale Spielereingaben als Attribute schreiben
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Verwenden Sie die Attribute als Eingaben für das Spiel
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Wenden Sie das Gaspedal auf das Fahrzeug des Spielers an
end
end
end)
end)
return module
Debugging
Es gibt einige neue Tools und Techniken, die Sie verwenden können, um ein serverautorisiertes Spiel zu debuggen.
Visualisierung der Serverautorisierung
Das Drücken von CtrlShiftF6 (Windows) oder ⌘ShiftF6 (Mac) öffnet die Visualisierung der Serverautorisierung im Studio, die mehrere wichtige Informationen anzeigt:
| Details | Beschreibung |
|---|---|
| Prozentsatz der erfolgreichen Vorhersagen von Instanzen | Der Prozentsatz der korrekt vorhergesagten Instanzen in den letzten 8 Sekunden. |
| Eingangsannahmerate | Der Prozentsatz aller Eingaben der Spieler, die pünktlich auf dem Server angekommen sind. Verspätete Eingaben senken diese Zahl. |
| Delta der Client-Server-Schritte | Die Anzahl der Frames zwischen dem Client und dem Server, einschließlich der Beitrittszeit des Clients. Die Stabilität dieser Zahl stellt die Stabilität Ihrer Verbindung zum Server dar. |
| RCC-Herzschlag-FPS | Die Bildrate der Simulation auf dem Server. Wenn diese Zahl unter 59 fällt, kann der Server nicht mit der Simulation Schritt halten und die Qualität des Spiels wird abnehmen. |
| Vorhergesagte Instanzanzahl | Die Anzahl der Instanzen, die Ihr Client vorhersagt. |
| Zählungen der Eingabeverlustgründe | Die Anzahl der Male, die der Server eine Eingabe aus jedem Grund verworfen hat:
|
Simulationsradius
Wenn Sie sich auf die automatische Vorhersage (Enum.PredictionMode.Automatic) verlassen, können Sie den Vorhersageradius um Ihren Spielercharakter sichtbar machen, indem Sie Sind Bereiche aktiviert in den Einstellungen von Studio aktivieren (AltS auf Windows; ⌥S auf Mac). Der grüne Zylinder zeigt den Bereich um Ihren Charakter an, in dem Instanzen vorhergesagt werden, und sein Radius wächst und schrumpft basierend auf den Leistungseigenschaften des Geräts.
