1 Punkte von GN⁺ 2024-11-24 | Noch keine Kommentare. | Auf WhatsApp teilen
  • 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.

Noch keine Kommentare.