Wie würde ein Spielstatus-Snapshot-System für vernetzte Echtzeitspiele implementiert?

12

Ich möchte ein einfaches Client-Server-Echtzeit-Multiplayer-Spiel als Projekt für meine Netzwerkklasse erstellen.

Ich habe viel über Echtzeit-Multiplayer-Netzwerkmodelle gelesen und verstehe die Beziehungen zwischen dem Client und dem Server sowie die Verzögerungskompensationstechniken.

Was ich tun möchte, ist etwas Ähnliches wie das Quake 3-Netzwerkmodell: Im Grunde speichert der Server eine Momentaufnahme des gesamten Spielzustands. Nach Eingang der Eingaben von den Clients erstellt der Server einen neuen Snapshot, der die Änderungen widerspiegelt. Anschließend werden die Unterschiede zwischen dem neuen und dem letzten Snapshot berechnet und an die Clients gesendet, damit diese synchronisiert werden können.

Dieser Ansatz scheint mir wirklich solide zu sein - wenn der Client und der Server eine stabile Verbindung haben, wird nur die minimale Datenmenge gesendet, die erforderlich ist, um sie synchron zu halten. Wenn der Client nicht mehr synchron ist, kann auch ein vollständiger Snapshot angefordert werden.

Ich kann jedoch keinen guten Weg finden, um das Schnappschuss-System zu implementieren. Es fällt mir wirklich schwer, mich von der Single-Player-Programmierarchitektur zu lösen und darüber nachzudenken, wie ich den Spielstatus so speichern kann, dass:

  • Alle Daten sind von der Logik getrennt
  • Zwischen Momentaufnahmen von Spielzuständen können Unterschiede berechnet werden
  • Spieleinheiten können immer noch einfach per Code manipuliert werden

Wie wird eine Snapshot- Klasse implementiert? Wie werden die Entitäten und ihre Daten gespeichert? Verfügt jede Client-Entität über eine ID, die mit einer ID auf dem Server übereinstimmt?

Wie werden Schnappschussdifferenzen berechnet?

Allgemein: Wie würde ein Snapshot-System mit Spielstatus implementiert?

Vittorio Romeo
quelle
4
+1. Dies ist ein bisschen zu weit gefasst für eine einzelne Frage, aber IMO ist es ein interessantes Thema, das grob in einer Antwort behandelt werden kann.
Kromster sagt Unterstützung Monica
Warum speichern Sie nicht einfach 1 Snapshot (die aktuelle Welt), speichern alle eingehenden Änderungen in diesem regulären Weltzustand und speichern die Änderungen in einer Liste oder Ähnlichem. Wenn Sie dann die Änderungen an alle Clients senden möchten, senden Sie einfach den Inhalt der Liste an alle Clients und löschen Sie die Liste. Beginnen Sie bei Null (Änderungen). Vielleicht ist dies nicht so gut wie das Speichern von 2 Schnappschüssen, aber mit diesem Ansatz müssen Sie sich keine Gedanken über Algorithmen machen, wie man 2 Schnappschüsse schnell verteilt.
Tkausl
Haben Sie dies gelesen: fabiensanglard.net/quake3/network.php - die Überprüfung des Quake 3-Netzwerkmodells beinhaltet eine Diskussion zur Implementierung.
Steven
Welche Art von Spiel versuchen Sie zu konstruieren? Das Netzwerk-Setup hängt stark von der Art des Spiels ab, das Sie machen. Ein RTS verhält sich in Bezug auf die Vernetzung nicht wie ein FPS.
AturSams

Antworten:

3

Sie können das Snapshot-Delta (Änderungen am vorherigen synchronisierten Status) berechnen, indem Sie zwei Snapshot-Instanzen beibehalten: die aktuelle und die zuletzt synchronisierte.

Wenn Client-Eingaben eingehen, ändern Sie den aktuellen Snapshot. Wenn dann Delta an Clients gesendet werden soll, berechnen Sie (rekursiv) den letzten synchronisierten Snapshot mit dem aktuellen Feld für Feld und berechnen und serialisieren Delta. Für die Serialisierung können Sie jedem Feld im Bereich seiner Klasse eine eindeutige ID zuweisen (im Gegensatz zum globalen Statusbereich). Client und Server sollten für den globalen Status dieselbe Datenstruktur verwenden, damit der Client versteht, worauf eine bestimmte ID angewendet wird.

Wenn Delta berechnet wird, klonen Sie den aktuellen Status und machen ihn zum letzten synchronisierten. Jetzt haben Sie den gleichen aktuellen und den letzten synchronisierten Status, aber verschiedene Instanzen, sodass Sie den aktuellen Status ändern und den anderen nicht beeinflussen können.

Dieser Ansatz kann einfacher zu implementieren sein, insbesondere mithilfe der Reflektion (wenn Sie einen solchen Luxus haben), kann aber auch dann langsam sein, wenn Sie den Reflektionsteil stark optimieren (indem Sie Ihr Datenschema so erstellen, dass die meisten Reflektionsaufrufe zwischengespeichert werden). Hauptsächlich, weil Sie zwei Kopien eines potenziell großen Zustands vergleichen müssen. Natürlich hängt es davon ab, wie Sie Vergleiche durchführen und welche Sprache Sie verwenden. Es kann in C ++ mit fest codiertem Komparator schnell sein, ist aber nicht so flexibel: Jede Änderung Ihrer globalen Statusstruktur erfordert eine Änderung dieses Komparators, und diese Änderungen sind in den ersten Projektphasen so häufig.

Ein anderer Ansatz ist die Verwendung von Dirty Flags. Jedes Mal, wenn Client-Eingaben eingehen, wenden Sie diese auf Ihre einzelne Kopie des globalen Status an und markieren die entsprechenden Felder als fehlerhaft. Wenn dann Clients synchronisiert werden sollen, serialisieren Sie (rekursiv) fehlerhafte Felder mit denselben eindeutigen IDs. (Kleiner) Nachteil ist, dass manchmal mehr Daten gesendet werden als unbedingt erforderlich: z. B. int field1wurde anfangs 0 zugewiesen, dann 1 (und als verschmutzt markiert) und danach erneut 0 zugewiesen (bleibt jedoch verschmutzt). Der Vorteil ist, dass Sie bei einer riesigen hierarchischen Datenstruktur diese nicht vollständig analysieren müssen, um Delta-Daten zu berechnen, sondern nur fehlerhafte Pfade.

Im Allgemeinen kann diese Aufgabe recht kompliziert sein, abhängig davon, wie flexibel die endgültige Lösung sein soll. Beispiel: Unity3D 5 (in Kürze) verwendet Attribute, um Daten anzugeben, die automatisch mit Clients synchronisiert werden sollen (sehr flexibler Ansatz, Sie müssen nur ein Attribut zu Ihren Feldern hinzufügen) und anschließend Code als generieren Nachbauschritt. Weitere Details hier.

Andriy Tylychko
quelle
2

Zunächst müssen Sie wissen, wie Sie Ihre relevanten Daten protokollkonform darstellen können. Dies hängt von den für das Spiel relevanten Daten ab. Ich werde ein RTS-Spiel als Beispiel verwenden.

Zu Netzwerkzwecken werden alle Entitäten im Spiel aufgelistet (z. B. Pickups, Einheiten, Gebäude, natürliche Ressourcen, zerstörbare Objekte).

Die Spieler müssen die für sie relevanten Daten haben (zum Beispiel alle sichtbaren Einheiten):

  • Sind sie am Leben oder tot?
  • Welcher Typ sind sie?
  • Wie viel Gesundheit haben sie noch?
  • Aktuelle Position, Rotation, Geschwindigkeit (Geschwindigkeit + Richtung), Pfad in naher Zukunft ...
  • Aktivität: Angreifen, Gehen, Bauen, Reparieren, Heilen, etc ...
  • Buff / Debuff Status Effekte
  • und möglicherweise andere Werte wie Mana, Schilde und was nicht?

Zuerst muss der Spieler den vollständigen Status erhalten, bevor er das Spiel betreten kann (oder alternativ alle Informationen, die für diesen Spieler relevant sind).

Jede Einheit hat eine ganzzahlige ID. Attribute werden aufgezählt und haben daher auch integrale Bezeichner. Die Einheiten-IDs müssen nicht 32 Bit lang sein (es könnte sein, wenn wir nicht sparsam sind). Es könnten sehr gut 20 Bits sein (wobei 10 Bits für die Attribute übrig bleiben). Die ID der Einheiten muss eindeutig sein. Sie kann sehr gut von einer Marke zugewiesen werden, wenn die Einheit instanziiert und / oder der Spielwelt hinzugefügt wird (Gebäude und Ressourcen gelten als unbewegliche Einheit, und Ressourcen können eine ID zugewiesen werden, wenn die Karte angezeigt wird geladen ist).

Der Server speichert den aktuellen globalen Status. Der zuletzt aktualisierte Status jedes Spielers wird durch einen Zeiger auf eine listder letzten Änderungen dargestellt (alle Änderungen, nachdem der Zeiger noch nicht an diesen Spieler gesendet wurde). Änderungen werden zum listZeitpunkt ihres Auftretens hinzugefügt . Sobald der Server mit dem Senden der letzten Aktualisierung fertig ist, kann er beginnen, die Liste zu durchlaufen: Der Server bewegt den Zeiger des Spielers entlang der Liste zu seinem Ende, sammelt alle Änderungen auf dem Weg und legt sie in einem Puffer ab, der an gesendet wird Der Spieler (dh das Format des Protokolls kann ungefähr so ​​aussehen: unit_id; attr_id; new_value) Neue Einheiten gelten ebenfalls als Änderungen und werden mit all ihren Attributwerten an die empfangenden Spieler gesendet.

Wenn Sie keine Sprache mit einem Garbage Collector verwenden, müssen Sie einen verzögerten Zeiger einrichten, der dann den veraltetesten Player-Zeiger in der Liste aufholt und Objekte auf dem Weg freigibt. Sie können sich erinnern, welcher Spieler innerhalb eines Prioritätshaufens am veraltetesten ist, oder einfach iterieren und frei, bis der faule Zeiger gleich ist (dh auf denselben Gegenstand zeigt wie einer der Zeiger des Spielers).

Einige Fragen, die Sie nicht aufgeworfen haben und die ich interessant finde, sind:

  1. Sollen die Kunden überhaupt einen Schnappschuss mit allen Daten erhalten? Was ist mit Gegenständen außerhalb ihrer Sichtlinie? Was ist mit Nebel des Krieges in RTS-Spielen? Wenn Sie alle Daten senden, kann der Client gehackt werden, um Daten anzuzeigen, die für den Player nicht verfügbar sein sollten (abhängig von anderen Sicherheitsmaßnahmen, die Sie ergreifen). Wenn Sie nur relevante Daten senden, ist das Problem behoben.
  2. Wann ist es wichtig, Änderungen zu senden, anstatt alle Informationen zu senden? Erhalten wir in Anbetracht der Bandbreite, die auf modernen Maschinen verfügbar ist, irgendetwas vom Senden eines "Deltas", anstatt alle Informationen zu senden, wenn ja, wann?
AturSams
quelle