Ich arbeite an einem isometrischen 2D-Spiel mit moderatem Multiplayer-Modus, bei dem ungefähr 20 bis 30 Spieler gleichzeitig mit einem permanenten Server verbunden sind. Ich hatte einige Schwierigkeiten, eine gute Implementierung für die Bewegungsvorhersage zu finden.
Physik / Bewegung
Das Spiel hat keine echte physische Implementierung, sondern verwendet die Grundprinzipien, um Bewegung zu implementieren. Anstatt fortlaufend Eingaben abzufragen, werden Statusänderungen (z. B. / Maus runter / rauf / Verschieben) verwendet, um den Status der Charakterentität zu ändern, die der Spieler kontrolliert. Die Richtung des Spielers (dh / Nordosten) wird mit einer konstanten Geschwindigkeit kombiniert und in einen wahren 3D-Vektor umgewandelt - die Geschwindigkeit der Entität.
In der Hauptspielschleife wird "Update" vor "Draw" aufgerufen. Die Aktualisierungslogik löst eine "Physikaktualisierungsaufgabe" aus, die alle Entitäten mit einer Geschwindigkeit ungleich Null verfolgt, wobei eine sehr grundlegende Integration zum Ändern der Entitätenposition verwendet wird. Beispiel: entity.Position + = entity.Velocity.Scale (ElapsedTime.Seconds) (wobei "Seconds" ein Gleitkommawert ist, der gleiche Ansatz jedoch für Ganzzahlwerte in Millisekunden funktioniert).
Der entscheidende Punkt ist, dass keine Interpolation für die Bewegung verwendet wird - die rudimentäre Physik-Engine kennt kein Konzept eines "vorherigen Zustands" oder "aktuellen Zustands", sondern nur eine Position und eine Geschwindigkeit.
Statusänderungs- und Aktualisierungspakete
Wenn sich die Geschwindigkeit des Charakters, den der Spieler kontrolliert, ändert, wird ein "Avatar bewegen" -Paket an den Server gesendet, das den Aktionstyp (Stehen, Gehen, Laufen), die Richtung (Nordosten) und die aktuelle Position des Charakters enthält. Dies unterscheidet sich von der Funktionsweise von 3D-Ego-Spielen. In einem 3D-Spiel kann sich die Geschwindigkeit (Richtung) von Bild zu Bild ändern, wenn sich der Spieler bewegt. Das Senden jeder Zustandsänderung würde effektiv ein Paket pro Rahmen übertragen, was zu teuer wäre. Stattdessen scheinen 3D-Spiele Statusänderungen zu ignorieren und "Statusaktualisierungs" -Pakete in einem festen Intervall zu senden - etwa alle 80-150 ms.
Da Geschwindigkeits- und Richtungsaktualisierungen in meinem Spiel viel seltener vorkommen, kann ich jede Statusänderung nicht mehr senden. Obwohl alle physikalischen Simulationen mit der gleichen Geschwindigkeit und deterministisch ablaufen, ist die Latenz nach wie vor ein Problem. Aus diesem Grund sende ich routinemäßig Positionsaktualisierungspakete aus (ähnlich wie bei einem 3D-Spiel), allerdings viel seltener - derzeit alle 250 ms, aber ich vermute, mit einer guten Vorhersage kann ich es leicht in Richtung 500 ms steigern. Das größte Problem ist, dass ich jetzt von der Norm abgewichen bin - alle anderen Online-Dokumentationen, Anleitungen und Beispiele senden Routine-Updates und interpolieren zwischen den beiden Zuständen. Es scheint mit meiner Architektur nicht kompatibel zu sein, und ich muss einen besseren Algorithmus für die Bewegungsvorhersage entwickeln, der einer (sehr einfachen) "vernetzten Physik" -Architektur näher kommt.
Der Server empfängt dann das Paket und bestimmt die Geschwindigkeit des Spielers anhand seines Bewegungstyps anhand eines Skripts (Kann der Spieler laufen? Ermittelt die Laufgeschwindigkeit des Spielers). Sobald es die Geschwindigkeit hat, kombiniert es sie mit der Richtung, um einen Vektor zu erhalten - die Geschwindigkeit des Objekts. Einige Cheat-Erkennungen und grundlegende Überprüfungen werden durchgeführt, und die Entität auf der Serverseite wird mit der aktuellen Geschwindigkeit, Richtung und Position aktualisiert. Grundlegende Drosselung wird auch durchgeführt, um zu verhindern, dass Spieler den Server mit Bewegungsanforderungen überfluten.
Nach dem Aktualisieren seiner eigenen Entität sendet der Server ein "Avatar-Positionsaktualisierungs" -Paket an alle anderen Spieler in Reichweite. Das Positionsaktualisierungspaket wird verwendet, um die clientseitigen Physiksimulationen (Weltzustand) der entfernten Clients zu aktualisieren und eine Vorhersage und eine Verzögerungskompensation durchzuführen.
Vorhersage und Lag Compensation
Wie oben erwähnt, sind Kunden für ihre eigene Position maßgeblich. Außer im Falle von Betrug oder Anomalien wird der Avatar des Kunden niemals vom Server neu positioniert. Für den Avatar des Kunden ist keine Extrapolation ("Jetzt verschieben und später korrigieren") erforderlich - was der Spieler sieht, ist korrekt. Es ist jedoch eine Extrapolation oder Interpolation für alle entfernten Entitäten erforderlich, die sich bewegen. In der lokalen Simulations- / Physik-Engine des Kunden ist eindeutig eine Art Vorhersage und / oder Verzögerungskompensation erforderlich.
Probleme
Ich habe mit verschiedenen Algorithmen zu kämpfen und habe eine Reihe von Fragen und Problemen:
Soll ich extrapolieren, interpolieren oder beides? Mein "Bauchgefühl" ist, dass ich eine reine Extrapolation basierend auf der Geschwindigkeit verwenden sollte. Die Zustandsänderung wird vom Client empfangen, der Client berechnet eine "vorhergesagte" Geschwindigkeit, die die Verzögerung ausgleicht, und das reguläre Physiksystem erledigt den Rest. Es fühlt sich jedoch im Widerspruch zu allen anderen Beispielcodes und Artikeln an - sie scheinen alle eine Reihe von Zuständen zu speichern und eine Interpolation ohne eine Physik-Engine durchzuführen.
Wenn ein Paket eintrifft, habe ich versucht, die Position des Pakets mit der Geschwindigkeit des Pakets über einen festgelegten Zeitraum (z. B. 200 ms) zu interpolieren. Ich nehme dann die Differenz zwischen der interpolierten Position und der aktuellen "Fehler" -Position, um einen neuen Vektor zu berechnen und diesen anstelle der gesendeten Geschwindigkeit auf dem Objekt zu platzieren. Die Annahme ist jedoch, dass in diesem Zeitintervall ein anderes Paket eintrifft, und es ist unglaublich schwierig zu "erraten", wann das nächste Paket eintrifft - zumal nicht alle in festen Intervallen eintreffen (dh auch / state-Änderungen). Ist das Konzept grundlegend fehlerhaft oder ist es korrekt, erfordert jedoch einige Korrekturen / Anpassungen?
Was passiert, wenn ein Remote-Player stoppt? Ich kann die Entität sofort stoppen, aber sie wird an der "falschen" Stelle positioniert, bis sie sich wieder bewegt. Wenn ich einen Vektor abschätze oder zu interpolieren versuche, habe ich ein Problem, weil ich den vorherigen Status nicht speichere - die Physik-Engine kann nicht sagen, dass Sie anhalten müssen, nachdem Sie Position X erreicht haben. Es versteht einfach eine Geschwindigkeit, nichts Komplexeres. Ich zögere es, der Entities- oder Physics-Engine die "Packet Movement State" -Informationen hinzuzufügen, da dies gegen grundlegende Designprinzipien verstößt und den Netzwerkcode über den Rest der Game-Engine verteilt.
Was soll passieren, wenn Entitäten kollidieren? Es gibt drei Szenarien: Der steuernde Spieler kollidiert lokal, zwei Entitäten kollidieren auf dem Server während einer Positionsaktualisierung oder eine Remote-Entitätsaktualisierung kollidiert auf dem lokalen Client. In allen Fällen bin ich mir nicht sicher, wie ich mit der Kollision umgehen soll - abgesehen vom Schummeln sind beide Zustände "korrekt", jedoch zu unterschiedlichen Zeitpunkten. Im Fall einer entfernten Entität ist es nicht sinnvoll, sie durch eine Wand zu ziehen. Daher führe ich auf dem lokalen Client eine Kollisionserkennung durch und veranlasse, dass sie "stoppt". Basierend auf dem obigen Punkt 2 könnte ich einen "korrigierten Vektor" berechnen, der kontinuierlich versucht, das Objekt "durch die Wand" zu bewegen, was niemals gelingt - der entfernte Avatar bleibt dort hängen, bis der Fehler zu hoch wird und "einrastet" Position. Wie funktionieren Spiele um dieses Problem herum?