Als ich beschloss, den Multiplayer von Warman von Grund auf neu zu schreiben, entschied ich mich für Lockstep-Simulation. Die Idee ist einfach: Synchronisiere nicht den Spielzustand, synchronisiere die Eingaben. Jeder Client führt exakt dieselbe Simulation aus. Solange die Eingaben identisch sind und die Simulation deterministisch ist, bleibt der Spielzustand synchron, ohne ihn jemals über das Netzwerk zu senden.
Das war eine praktische Entscheidung. Warman kann Hunderte von Gegnern, Projektilen, Bodeneffekten und Modifikatoren gleichzeitig aktiv haben. All diesen Zustand jeden Frame zu synchronisieren wäre teuer. Mit Lockstep sind die Netzwerkkosten konstant, unabhängig von der Spielkomplexität. Ob 10 Gegner oder 500, dieselben kleinen Input-Pakete werden gesendet.
Der Input-Befehl
Jeden Tick erzeugt jeder Spieler einen PlayerInputCommand. Dieses Struct enthält alles, was der Server wissen muss: einen Bewegungsvektor, eine Cursorposition relativ zum Spieler (zum Zielen), Boolean-Flags für jeden Skill-Slot und ein Interaktions-Flag. Dazu kommt ein Array von UI-Befehlen für Dinge wie Items kaufen, Ausrüstung anlegen oder Beute verkaufen.
Simulations-Ticks
Der Host ist autoritativ. Er sammelt Eingaben aller verbundenen Spieler in einen Input-Buffer und baut daraus einen SimulationTick, ein Struct, das die Tick-Nummer plus einen PlayerInputCommand pro Spieler-Slot (bis zu 8) enthält. Vor dem Schreiben markiert ein Bitmasken-Byte, welche Slots tatsächlich Input haben, sodass leere Spieler-Slots null Bytes beitragen. Der zusammengebaute Tick wird komprimiert und an alle Clients gesendet.
Clients empfangen diese Ticks und speisen sie der Reihe nach in ihre lokale Simulation ein. Die Simulation schreitet Tick für Tick voran und wendet die Eingabe jedes Spielers auf seinen Charakter an. Da die Simulation deterministisch ist und die Eingaben identisch sind, berechnen alle Clients dasselbe Ergebnis.
Input-Delay
Lockstep hat ein bekanntes Problem: Man kann einen Tick nicht simulieren, bis alle Eingaben für diesen Tick eingetroffen sind. Wenn die Eingabe eines Spielers zu spät kommt, warten alle. Die Standardlösung ist Input-Delay. Wenn ein Spieler eine Taste drückt, wird diese Eingabe nicht sofort wirksam, sondern für einen zukünftigen Tick eingeplant.
Warman berechnet den Input-Delay dynamisch basierend auf dem gemessenen One-Way-Ping zum Host. Die Formel teilt den Ping durch die Tick-Länge (1000ms / Tick-Rate) und rundet auf, dann addiert einen konfigurierbaren Jitter-Puffer. Bei 20 Ticks pro Sekunde mit 50ms Ping sind das etwa 2 Ticks Verzögerung, also 100ms. Unter 150ms merken die meisten Spieler nichts. Im Einzelspieler oder als Host ohne verbundene Mitspieler sinkt der Delay auf 1 Tick.
Fehlende Eingaben
Netzwerk-Jitter bedeutet, dass Eingaben manchmal zu spät ankommen. Anstatt die Simulation anzuhalten, füllt der Host fehlende Eingaben mit einer Schätzung, er kopiert die Bewegungsgeschwindigkeit und Cursorposition aus der letzten bekannten Eingabe des Spielers, setzt aber alle Aktionstasten auf null. Der Spieler läuft und zielt weiter in dieselbe Richtung, feuert aber keine Skills ab und interagiert mit nichts. Das verhindert ein Stocken der Simulation und schützt gleichzeitig vor Phantom-Tastendrücken.
Transport-Abstraktion
Warman unterstützt zwei Netzwerk-Transporte hinter einem einzigen IGameTransport-Interface: LiteNetLib (direktes UDP) für Nicht-Steam-Builds und Steam P2P für Steam-Nutzer. Der Steam-Transport nutzt Valves Relay-Netzwerk, das NAT-Traversal übernimmt, sodass Spieler keine Ports weiterleiten müssen. Die Transport-Wahl ist automatisch. Wenn der Spieler über Steam gestartet hat, bekommt er P2P. Andernfalls direktes UDP mit einer expliziten IP-Adresse.
Beide Transporte unterstützen zuverlässige geordnete Zustellung (für Simulations-Ticks und Lobby-Management) und zuverlässige ungeordnete Zustellung (für Chat und Spieler-Inputs). Die Abstraktion bedeutet, dass die Netzwerkschicht darüber sich nicht darum kümmert, welcher Transport aktiv ist. Host starten, als Client verbinden, Daten senden und Verbindungsabbrüche behandeln, alles läuft über dasselbe Interface.
Wenn der Host die Verbindung verliert
Eine nette Eigenschaft von Lockstep ist, dass jeder Client bereits den vollständigen Spielzustand hat. Wenn der Host mitten im Spiel die Verbindung verliert, muss der Client nicht alles abbrechen. Stattdessen schaltet er sich in den Einzelspieler-Modus: Der Transport wird heruntergefahren, der Client befördert sich zum Host, andere Spieler werden entfernt und die Simulation läuft lokal weiter. Aus Sicht des Spielers verschwinden die Freunde und eine Chat-Nachricht sagt „Spiel wird im Einzelspieler fortgesetzt." Kein Fortschritt geht verloren.
Das funktioniert nur im laufenden Spiel. Wenn der Host während der Lobby-Phase die Verbindung verliert (bevor die Simulation gestartet hat), gibt es nichts fortzusetzen. Der Client trennt normal die Verbindung.
Determinismus
Das gesamte System hängt vom Determinismus ab. Wenn zwei Clients aus denselben Eingaben ein unterschiedliches Ergebnis berechnen, desynct das Spiel und alles bricht zusammen. Das bedeutet: kein System.Random, kein UnityEngine.Random, keine Gleitkommaoperationen, die sich zwischen Plattformen unterscheiden könnten. Warman verwendet einen eigenen PRNG (MasterRandom), der vom Spiel-Seed gespeist wird, und aller Gameplay-Code nutzt ausschließlich diesen. Der Zufallszahlengenerator ist ein subtraktiver Generator basierend auf Knuths Algorithmus, gewählt weil er einfach, schnell und auf Windows, macOS und Linux identische Ergebnisse liefert.