- In einer schnellen FPS-Umgebung ist spät eintreffende Statusinformation nur wenig wertvoll, daher setzt Quake 3 auf ein UDP/IP-zentriertes Design, um die Latenz zu senken
- NetChannel abstrahiert die Kommunikation über verlustbehaftetes UDP, und der Server berechnet mithilfe pro Client gespeicherter Snapshots nur die benötigten Statusunterschiede neu
- Der Server verwendet den Master Gamestate, die letzten 32 Gamestates und einen Dummy-Gamestate gemeinsam, um vollständige Updates und Delta-Updates mit demselben Verfahren zu erzeugen
- Fehlt ein Client-ACK, vergleicht der Server den zuletzt bestätigten Snapshot mit dem aktuellen Zustand und packt fehlende Änderungen und neue Änderungen in eine Nachricht
- Auch ohne eingebaute Introspection in C werden mit
netField_t und Makros Feldunterschiede gefunden, und NetChannel vermeidet mit vorab aufgeteilten 1400-Byte-Paketen Router-Fragmentierung
Netzwerkmodell auf Basis von UDP/IP
- Das Netzwerkmodell von Quake 3 gilt als einer der elegantesten Teile der Engine; auf niedriger Ebene wird die Kommunikation durch das NetChannel-Modul abstrahiert, das zuerst in Quake World auftauchte
- In schnellen Spielen werden beim ersten Senden verpasste Informationen schnell zu veralteten Informationen, deshalb ist es vorteilhafter, den neuesten Zustand zu senden, als erneut zu senden
- Deshalb gibt es in der Engine keine TCP/IP-Spuren, weil die durch zuverlässige Übertragung entstehende Latenz als zu teuer angesehen wird
- Dem Netzwerk-Stack werden zwei sich gegenseitig ausschließende Schichten hinzugefügt
- Verschlüsselung mit einem vorab geteilten Schlüssel
- Kompression mit einem vorab berechneten Huffman-Schlüssel
- Der Server kompensiert die Unzuverlässigkeit, während er die Größe der UDP-Datagramme verringert
- Er erzeugt Delta-Pakete anhand der Snapshot-Historie
- Er findet und sendet nur geänderte Felder per Memory-Introspection-Ansatz
Rollen von Server und Client
- Der Ablauf auf Client-Seite ist einfach
- In jedem Frame werden Befehle an den Server gesendet
- Gamestate-Updates werden vom Server empfangen
- Der Server muss den Master Gamestate an jeden Client weitergeben und dabei sogar verlorene UDP-Pakete berücksichtigen
- Der Kernmechanismus besteht aus drei Elementen
- Master Gamestate: der allgemein gültige Spielzustand; Client-Befehle kommen über NetChannel herein, werden in
event_t umgewandelt und ändern dann auf dem Server den Spielzustand
- Die letzten 32 client-spezifischen Gamestates: über das Netzwerk gesendete Zustände werden in einem Ringpuffer gespeichert und Snapshots genannt
- Dummy-Gamestate: ein Zustand, dessen alle Felder 0 sind und der als Referenz für die Delta-Erzeugung dient, wenn kein vorheriger Zustand existiert
- Aus diesen drei Elementen erzeugt der Server die Update-Nachricht, die an NetChannel übergeben wird
- Weil viele client-spezifische Gamestates gehalten werden müssen, ist der Speicherverbrauch hoch
- Als Messwert werden bei 4 Spielern 8 MB verwendet
Vollständige und partielle Updates mit Snapshots erzeugen
- Das Beispiel beschreibt eine Situation, in der ein Update an Client1 gesendet wird und der Zustand von Client2 aus vier Feldern besteht:
pos[X], pos[Y], pos[Z], health
- Die Kommunikation erfolgt über UDP/IP, und im Internet können Nachrichten häufig verloren gehen
-
Erster Server-Frame
- Der Server übernimmt alle von den Clients erhaltenen Updates in den Master Gamestate und überträgt dann den Zustand an Client1
- Das Netzwerkmodul folgt jedes Mal demselben Verfahren
- Der Master Gamestate wird in den nächsten Slot der Client-Historie kopiert
- Der kopierte Snapshot wird mit einem anderen Snapshot verglichen
- Beim ersten Update gibt es in der Historie von Client1 keinen gültigen Snapshot, daher wird mit dem Dummy-Snapshot verglichen
- Da im Dummy-Snapshot alle Felder 0 sind, ergibt sich ein vollständiges Update
- Vor jedem Feld steht ein Bit-Marker, der anzeigt, ob es geändert wurde
- Das vollständige Beispiel-Update verwendet 132 Bit
- Das Format ist
[1 A_on32bits 1 B_on32bits 1 B_on32bits 1 C_on32bits]
-
Zweiter Server-Frame
- Im nächsten Frame bewegt sich Client2 entlang der Y-Achse und der Wert von
pos[1] wird zu E
- Client1 hat den Empfang des vorherigen Updates per ACK bestätigt, daher ist Snapshot1 im Zustand ACK
- Der Server kopiert den Master Gamestate in den nächsten Historienslot, erstellt Snapshot2 und vergleicht ihn mit dem gültigen Snapshot1
- Dadurch wird nur das geänderte
pos[1] = E über das Netzwerk gesendet
- Weil jedes Feld einen Bit-Marker hat, verwendet dieses partielle Update 36 Bit
- Das Format ist
[0 1 32bitsNewValue 0 0]
-
Dritter Server-Frame
- Im nächsten Frame verliert Client2 Gesundheit und
health = H
- Client1 bestätigt das letzte Update nicht per ACK
- Möglicherweise ist das UDP-Paket des Servers verloren gegangen, oder das ACK des Clients ist verloren gegangen
- In beiden Fällen ist dieser Snapshot nicht verwendbar
- Der Server kopiert den Master Gamestate in den nächsten Slot, erstellt Snapshot3 und vergleicht ihn mit dem zuletzt per ACK bestätigten Snapshot1
- Die gesendete Nachricht ist ein partielles Update und enthält sowohl die frühere Änderung
pos[1] = E als auch die neue Änderung health = H
- Wenn Snapshot1 zu alt ist und nicht mehr verwendet werden kann, sendet die Engine erneut ein vollständiges Update auf Basis des Dummy-Snapshots
Wie Verluste mit demselben Verfahren kompensiert werden
- Die Einfachheit des Snapshot-Systems liegt darin, dass derselbe Algorithmus automatisch zwei Aufgaben übernimmt
- Erzeugung vollständiger Updates oder partieller Updates
- Erneutes Senden früherer nicht empfangener Informationen zusammen mit neuen Informationen in einer Nachricht
- Paketverlust bei UDP wird nicht in einem separaten komplexen Ablauf behandelt; stattdessen wird der Unterschied zwischen dem zuletzt per ACK bestätigten Snapshot und dem aktuellen Master Gamestate berechnet und so kompensiert
- Wenn kein vorheriger Zustand vorhanden oder verwendbar ist, wird zur Wiederherstellung der vollständige Zustand auf Basis des Dummy-Snapshots gesendet
Wie Feldunterschiede in C gefunden werden
- Quake 3 hat in C keine Introspection, aber die Position jedes Felds wird vorab mit einem
netField_t-Array und Präprozessor-Direktiven aufgebaut
netField_t enthält Feldname, Offset und Bitzahl
- Das Makro
NETF(x) nutzt den Stringizing-Operator und die Offset-Berechnung für entityState_t, damit Feldinformationen kurz geschrieben werden können
- Die Beispielstruktur sieht so aus
typedef struct { char *name; int offset; int bits; } netField_t;
// using the stringizing operator to save typing...
#define NETF(x) #x,(int)&((entityState_t*)0)->x
netField_t entityStateFields[] = {
{ NETF(pos.trTime), 32 },
{ NETF(pos.trBase[0]), 0 },
{ NETF(pos.trBase[1]), 0 },
...
}
- Die vollständige Implementierung steht in einem Teil von MSG_WriteDeltaEntity
- Quake 3 interpretiert die Bedeutung des Vergleichsziels nicht, sondern folgt Index, Offset und Größe von
entityStateFields, um Unterschiede über das Netzwerk zu senden
Warum im Voraus in 1400 Byte aufgeteilt wird
- Das NetChannel-Modul teilt Nachrichten in 1400-Byte-Stücke auf, obwohl die maximale Größe eines UDP-Datagramms 65507 Byte beträgt
- Der relevante Code befindet sich in Netchan_Transmit
- Da die MTU der meisten Netzwerke 1500 Byte beträgt, ist die Aufteilung in 1400 Byte eine Entscheidung, um zu verhindern, dass Router auf dem Internetpfad Pakete fragmentieren
- Es gibt zwei Gründe, Router-Fragmentierung zu vermeiden
- Beim Eintritt ins Netzwerk muss der Router das Paket festhalten, während er es fragmentiert
- Beim Austritt aus dem Netzwerk müssen alle Fragmente des Datagramms abgewartet werden, bevor die kostspielige Reassemblierung erfolgen kann
Nachrichten, die unbedingt zugestellt werden müssen
- Das Snapshot-System kompensiert im Netzwerk verlorene UDP-Datagramme, aber einige Nachrichten und Befehle müssen unbedingt zugestellt werden
- Dazu gehören Fälle, in denen ein Spieler das Spiel verlässt oder der Server vom Client das Laden eines neuen Levels verlangt
- Diese Garantie wird von NetChannel abstrahiert
Verwandte Lektüre
Noch keine Kommentare.