Dieser Leitfaden skizziert verschiedene Techniken zur Erstellung hochwertiger, reibungsloser Mehrspielerspiele unter Verwendung des Serverautoritätsmodells.
Prädiktive Instanz Erstellung (Instance Stitching)
Instanz Stitching ermöglicht es Client-Skripten, Instances innerhalb von RunService:BindToSimulation()-Callbacks prädiktiv zu erstellen. Der Client erstellt die Instance sofort, ohne auf eine Server-Round-Trip zu warten; wenn die autoritative Kopie des Servers eintrifft, werden die client-erstellte Instanz und die autoritative Kopie des Servers zu einer zusammengeführt. Aus der Perspektive Deines Skripts existiert die Instance sofort und ist konsistent mit dem Server.
Instanz-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 replizieren wird, die der Client benötigt (nebst aller Effekte, die sie auf die Welt hatten), verursacht dieser Prozess mindestens eine Round-Trip-Latenz aufgrund der Serverkommunikation. Beispiele hierfür sind das Abfeuern eines Raketenwerfers und das Erstellen von physikalischen Einschränkungen — ohne Stitching wird der Client die Rakete in großer Entfernung erscheinen sehen oder es wird Ruckeln auftreten, wenn die neuen Einschränkungen an ihn repliziert werden.
Technisches Verhalten
Instanz-Stitching funktioniert, indem sowohl der Client als auch der Server dieselbe 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 jeden Rahmen zurückgesetzt wird.
- Für Instance.new() — Die Quelle ist das Skript selbst (zwei Skripte mit demselben Text gelten als unterschiedlich).
- Für Instance.fromExisting() — Die Quelle ist die Instance, auf der Du Instance.fromExisting() aufrufst.
- Für Instance:Clone() — Jede geklonte Instanz verwendet die GUID der Quellinstanz als Kontext-Saat.
Wenn Client und Server sich über die Eingaben einig sind, erzeugen sie übereinstimmende GUIDs und das Stitching gelingt.
Implementierung
Um Instanz-Stitching zu nutzen, rufe Instance.new(), Instance:Clone() oder Instance.fromExisting() innerhalb eines BindToSimulation()-Callbacks aus einem ModuleScript auf, das sowohl auf dem Client als auch auf dem Server benötigt wird. Nichts anderes ist auf Deiner Seite erforderlich; das System kümmert sich automatisch um die GUID-Zuweisung und Versöhnung.
Du kannst frei Eigenschaften, die keine Simulation-Zugriffe sind, wie Name, Size oder Parent auf einer Instanz festlegen, bevor sie in das DataModel eingeordnet wird.
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 Datenmodell; alle Änderungen außerhalb der Simulation führen nach diesem Fehler auf
-- Teil existiert sofort auf dem Client und wird mit dem Server versöhnt
end)
end
return SimulationInstance:Clone() und Instance.fromExisting() funktionieren korrekt, wenn die Quellinstanz sowohl dem Client als auch dem Server repliziert wurde; beide Seiten klonen von übereinstimmenden Quell-GUIDs und erzeugen übereinstimmende prädiktive GUIDs.
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
-- Geklonte Hierarchie wird mit der autoritativen Kopie des Servers verbunden
end)
end
return SimulationPositionsglättung
Du kannst die Position von falsch vorhergesagten synchronisierten Objekten visuell glätten, indem Du ein anderes Objekt renderst als das, das simuliert wird.
- Mache das simulierte Objekt unsichtbar.
- Erstelle ein Renderer-Objekt als masseloses, nicht kollidierendes, nur visuelles Klon, um das simulierte Objekt zu verfolgen.
- Füge ein Skript zum Renderer-Objekt hinzu, das die Position des unsichtbaren, simulierten Objekts sanft verfolgt. Diese Trennung zwischen Rendering und Simulation gibt dir die Möglichkeit, die Position des Renderer-Objekts zu ändern, um ein visuell glattes Erlebnis zu schaffen.
Im folgenden Beispiel Script verfolgt das gerenderte Objekt (Elternteil) sanft das simulierte Objekt. Das gerenderte Objekt befindet sich immer leicht "hinter" dem simulierten Objekt, was normalerweise in Ordnung ist, in bestimmten Situationen jedoch unerwünscht sein kann.
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- Objekt, das sanft verfolgt werden soll
local smoothTarget:BasePart = workspace.SimulatedPart
-- Visuelles Objekt, das geglättet wird
local renderer:BasePart = script.Parent
-- Zeit zur Glättung; kleiner bedeutet schneller
local smoothTime = 0.07
-- Speichere Daten, die zur Berechnung der glatten Position erforderlich sind
local smoothVelocity = Vector3.new()
-- Deaktiviere die Physik des Renderer-Objekts
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Verfolge das Zielobjekt sanft
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 das Aktivieren und Deaktivieren der Positionsglättung für den Fußball intelligenter zu gestalten. Konkret glättet der Fußball seine Position nur, wenn der simulierte Ball weit genug vom 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 reibungslos, nachdem der simulierte Ball unerwartet an einen neuen Standort gesprungen ist, wahrscheinlich aufgrund eines Netzwerkartefakts oder einer serverseitigen Änderung.
Animationscode schreiben
Unter der Serverautorisierung kann die Simulation des Clients zurückgesetzt und neu simuliert werden, wenn der Server eine Fehlschätzung korrigiert. Während des Rollbacks wird der Animationszustand zurückgespult, was bedeutet, dass AnimationTrack, die Du in früheren Rahmen zwischengespeichert hast, möglicherweise nicht mehr gültig ist.
Animationslogik spiegeln
Wie bei jeder grundlegenden Gameplay-Logik muss die Logik zur Steuerung der Animationen zwischen Server und Client synchronisiert sein; andernfalls kann es zu Fehleinschätzungen und ruckelndem Verhalten kommen. Siehe Simulationssynchronsierung 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.
Vermeide Track-Caching
Ein gängiges Muster in Skripten ohne Serverautorisierung besteht darin, AnimationTrack-Objekte zur Ladezeit zwischenzuspeichern und sie unbegrenzt wiederzuverwenden. Dieses Muster versagt in einem serverautorisierenden Spiel, wenn der Server eine Fehlschätzung korrigiert und der Client seine Simulation mit korrigierten Daten zurückspult/wiedergibt. Wenn Dein Skript noch eine Referenz auf einen gestoppten oder ersetzten Track hält, werden Aufrufe wie AdjustWeight() oder AdjustSpeed() auf einem Track wirken, der nicht länger visuell dargestellt wird.
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")
-- Animationstracks zwischenspeichern
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)Anstelle von Track-Objekten, speichere die Animations-IDs (oder Animation-Instanzen) und lasse den Animator für den Live-Track abfragen, wann immer Du mit ihm interagieren musst. Zwei APIs stehen dafür zur Verfügung:
- Animator:GetTrackByAnimationId() — Gibt den derzeit aktiven Track für eine spezifische Animations-ID zurück, oder nil, wenn es keine aktiven Animationen mit dieser ID gibt. Verwende dies, wenn Du weißt, welche spezifische Animation Du suchst.
- Animator:GetPlayingAnimationTracks() — Gibt alle aktiven Tracks (spielend, ausblendend oder pausiert) zurück. Verwende dies, wenn Du über alles aktive iterieren musst (zum Beispiel, um alle Animationen zu stoppen oder Tracks nach bestimmten Kriterien zu finden).
ModuleScript mit dem Namen CustomAnimate in ReplicatedStorage:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Animationsreferenzen speichern (keine geladenen 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 CustomAnimateTöne und visuelle Effekte abspielen
In einer prädiktiven Simulation ist es möglich, Effekte oder Töne für Ereignisse zu triggern, von denen der Client vorhersagte, dass sie geschehen würden, die aber auf dem Server nie auftraten. Das Rendering-System sollte darauf vorbereitet sein, alle fehlerhaft vorhergesagten Effekte "rückgängig" zu machen. Zum Beispiel könnte ein Client vorhersagen, dass eine Granate explodiert und einen Partikeleffekt auslösen, aber wenn ein anderer Spieler die Granate entschärft, sollte der Client den Partikeleffekt verbergen.
Eine gute Strategie für das Rendering einer prädiktiven Simulation besteht darin, ein Zustandsmaschinenmuster innerhalb der Simulationsschleife zu synchronisieren und Änderungen des Zustands in einer Render-Schritt-Funktion darzustellen. Das folgende Beispiel simuliert eine Granate mit einem Zustandsmaschinenmuster:
local module = {}
module.GrenadeStates = {
Idle = 0,
Lit = 1,
Exploded = 2,
Defused = 3,
}
module.GrenadeExplodeTime = 3.0
module.Initialize = function(grenade)
RunService:BindToSimulation(function(deltaTime)
-- Initialisiere leeren Granatzustand
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Inkrementiere Granatentimer
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Explodiere gezündete 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 moduleMit der vorherigen Zustandsmaschine kannst Du Granateneffekte in einer RunService.RenderStepped-Verbindung innerhalb eines separaten Skripts basierend auf dem synchronisierten Granatenzustand darstellen:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Instanz hervorheben, um den Granatenzustand anzuzeigen
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")
-- Zündpartikel emittieren, wenn die Granate gezündet ist
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Explosionsemitter abspielen, wenn die Granate gerade explodiert ist
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
-- Farbe des Granaten-Highlights basierend auf Zustand und Zeit ändern
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)Entwurf um Netzwerklatenz
Bestimmte Gameplay-Mechaniken eignen sich besser für vernetzte Mehrspieler als andere Mechaniken. Spieler haben immer eine gewisse Verzögerung 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 reibungsloses Mehrspielerspiel zu erstellen, besteht darin, Dein Spiel mit diesen Einschränkungen im Hinterkopf zu entwerfen.
Zum Beispiel wird ein Spiel mit langsameren Beschleunigungszeiten bei der Bewegungssteuerung der Spieler reibungsloser erscheinen als eines mit höherer Beschleunigung, weil der Positionsunterschied, der durch die Netzwerklatenz der Spieler-Eingaben verursacht wird, geringer ist als in einem Spiel mit höherer Beschleunigung.
Ein weiteres Beispiel, eine Gameplay-Mechanik, bei der Spieler durch Drücken eines Eingabe eine große Explosion sofort auslösen können, wird mehr Netzwerkartefakte aufweisen, als wenn die Explosion nach der Eingabe verzögert wird, als ob man eine Zündschnur anzündet. Dies setzt die Neusimulation auf den Zündschnureffekt statt auf den Explosionseffekt, der ein weniger auffälliges Netzwerkartefakt ist.
Vorhersage anderer Spieler-Eingaben
Standardmäßig leitet Roblox die Eingaben jedes Clients nicht an jeden anderen Client weiter. Ob dies für Dein Spiel geeignet ist, hängt von seinem Design ab:
- Bei grundlegender humanoider Bewegung bedeutet das Standardverhalten, dass die Bewegungen anderer Spielercharaktere nicht aus dem autoritativen Serverzustand extrapoliert werden und infolgedessen andere Spielercharaktere nicht fehlerhaft vorhergesagt werden, sondern leicht in der Vergangenheit gerendert werden.
- In einem Rennspiel hingegen bedeutet das Standardverhalten, dass die Clients nicht wissen, ob andere Spieler das Gas oder andere Eingaben betätigen, sodass andere Autos hinter dem lokalen Spieler erscheinen können, selbst wenn sie tatsächlich voraus sind. Um dem entgegenzuwirken, kannst Du die Eingaben der Spieler in Attributen auf dem Server speichern und mit diesen synchronisierten Attributen auf der Clientseite arbeiten, indem Du RunService:BindToSimulation() verwendest, wie im folgenden Codesample und der Racing Vorlage demonstriert. Dieser Ansatz ermöglicht es Dir, Attribute als Eingaben für Deine Simulation zu verwenden, um vollständig replizierte Spieler-Eingaben zu haben.
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)
-- Schreibe andere Eingaben in Attribute...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Leite Inputs vom Server an alle Clients weiter
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Schreibe lokale Spieler-Eingaben als Attribute
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Verwende 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
-- Wende das Gas auf das Fahrzeug des Spielers an
end
end
end)
end)
return moduleDebugging
Es gibt einige neue Werkzeuge und Techniken, die Du nutzen kannst, um ein serverautorisiertes Spiel zu debuggen.
Visualisierer für Serverautorisierung
Durch Drücken von CtrlShiftF6 (Windows) oder ⌘ShiftF6 (Mac) öffnest Du den Visualisierer für Serverautorisierung von Studio, der mehrere wichtige Informationen anzeigt:
| Details | Beschreibung |
|---|---|
| Instanzvorhersage Erfolgsquote | Der Prozentsatz der korrekt vorhergesagten Instanzen der letzten 8 Sekunden. |
| Eingabeakzeptanzrate | Der Prozentsatz aller Eingaben der Spieler, die pünktlich auf dem Server eingegangen sind. Späte Eingaben senken diese Zahl. |
| Client-Server Schrittdelta | Die Anzahl der Frames zwischen dem Client und dem Server, einschließlich der Beitrittszeit des Clients. Die Stabilität dieser Zahl repräsentiert die Stabilität Deiner Verbindung zum Server. |
| RCC-Herzfrequenz FPS | Die Bildfrequenz der Simulation auf dem Server. Wenn diese Zahl unter 59 fällt, kann der Server mit der Simulation nicht mithalten und das Spiel wird an Qualität verlieren. |
| Vorhersageinstanzanzahl | Die Anzahl der Instanzen, die Dein Client vorhersagt. |
| Eingabeverlust-Gründe | Die Anzahl der Male, die der Server eine Eingabe aus jedem Grund fallen gelassen hat:
|
Simulationsradius
Wenn Du auf automatische Vorhersage (Enum.PredictionMode.Automatic) angewiesen bist, kannst Du den Vorhersageradius um Deinen Spielercharakter visualisieren, indem Du Regions aktiviert in den Einstellungen von Studio aktivierst (AltS auf Windows; ⌥S auf Mac). Der grüne Zylinder zeigt den Bereich um Deinen Charakter an, in dem Instanzen vorhergesagt werden, und sein Radius wächst und schrumpft basierend auf den Leistungseigenschaften des Geräts.
