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?
quelle
Antworten:
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 field1
wurde 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.
quelle
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):
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
list
der letzten Änderungen dargestellt (alle Änderungen, nachdem der Zeiger noch nicht an diesen Spieler gesendet wurde). Änderungen werden zumlist
Zeitpunkt 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:
quelle