
In den letzten zwei Wochen habe ich an einem Netzwerk-Engine für mein Spiel gearbeitet. Davor hatte ich überhaupt keine Kenntnisse über Netzwerktechnologien in Spielen, also habe ich zahlreiche Artikel gelesen und viele Experimente durchgeführt, um alle Konzepte zu verstehen und in der Lage zu sein, meine eigene Netzwerk-Engine zu schreiben.
In diesem Leitfaden möchte ich verschiedene Konzepte mit Ihnen teilen, die Sie lernen müssen, bevor Sie Ihre eigene Spiel-Engine schreiben, sowie die besten Ressourcen und Artikel für deren Studium.
Im Allgemeinen gibt es zwei Haupttypen von Netzwerkarchitekturen: Peer-to-Peer und Client-Server. Bei der Peer-to-Peer-Architektur (P2P) werden die Daten zwischen beliebigen Paaren von verbundenen Spielern übertragen, während bei der Client-Server-Architektur die Daten nur zwischen Spielern und dem Server übertragen werden.
Obwohl die Peer-to-Peer-Architektur in einigen Spielen nach wie vor verwendet wird, ist die Client-Server-Architektur der Standard: Sie ist einfacher zu implementieren, benötigt eine geringere Bandbreite und erleichtert den Schutz vor Betrug. Daher konzentrieren wir uns in diesem Leitfaden auf die Client-Server-Architektur.
Insbesondere interessieren uns autoritäre Server: In solchen Systemen hat der Server immer recht. Wenn ein Spieler denkt, dass er sich bei den Koordinaten (10, 5) befindet, der Server ihm jedoch mitteilt, dass er sich bei (5, 3) befindet, muss der Client seine Position durch die Position ersetzen, die der Server überträgt, und nicht umgekehrt. Der Einsatz autoritärer Server vereinfacht die Erkennung von Cheatern.
In spielorientierten Netzwerksystemen gibt es drei Hauptkomponenten:
- Transportprotokoll: wie die Daten zwischen Clients und Server übertragen werden.
- Anwendungsprotokoll: was von den Clients an den Server und vom Server an die Clients übertragen wird und in welchem Format.
- Anwendungslogik: wie die übertragenen Daten zur Aktualisierung des Zustands der Clients und des Servers verwendet werden.
Es ist sehr wichtig, die Rolle jedes Teils und die damit verbundenen Herausforderungen zu verstehen.
Transportprotokoll
Der erste Schritt besteht darin, ein Protokoll für den Datentransport zwischen dem Server und den Clients auszuwählen. Es gibt dafür zwei Internetprotokolle: und . Aber Sie können auch Ihr eigenes Transportprotokoll auf der Grundlage eines dieser Protokolle erstellen oder eine Bibliothek verwenden, in der sie verwendet werden.
Vergleich von TCP und UDP
Sowohl TCP als auch UDP basieren auf . Eine IP-Adresse ermöglicht die Übertragung eines Pakets vom Sender zum Empfänger, bietet jedoch keine Garantie dafür, dass das gesendete Paket irgendwann beim Empfänger ankommt, dass es ihn zumindest einmal erreicht und dass die Reihenfolge der Pakete korrekt ist. Darüber hinaus kann ein Paket nur eine begrenzte Datenmenge enthalten, die durch die Größe festgelegt ist. .
UDP ist lediglich eine dünne Schicht über IP. Folglich gelten dieselben Einschränkungen. Im Gegensatz dazu weist TCP viele Eigenschaften auf. Es gewährleistet eine zuverlässige, geordnete Verbindung zwischen zwei Knoten mit Fehlerüberprüfung. Daher ist TCP sehr praktisch und wird in vielen anderen Protokollen verwendet, wie beispielsweise in , und . Aber all diese Funktionen haben ihren Preis: .
Um zu verstehen, warum diese Funktionen Verzögerungen verursachen können, muss man wissen, wie TCP funktioniert. Wenn ein Sender ein Paket an einen Empfänger überträgt, erwartet er eine Bestätigung (ACK). Wenn er diese nach einer bestimmten Zeit nicht erhält (weil das Paket oder die Bestätigung verloren gegangen sind oder aus anderen Gründen), sendet er das Paket erneut. Darüber hinaus garantiert TCP die Empfangsreihenfolge der Pakete, sodass alle anderen Pakete nicht verarbeitet werden können, bis das verlorene Paket eingetroffen ist, auch wenn sie bereits vom Empfänger erhalten wurden.
Wie Sie wahrscheinlich verstehen, ist die Verzögerung in Mehrspieler-Spielen sehr wichtig, insbesondere in so dynamischen Genres wie FPS. Aus diesem Grund verwenden viele Spiele UDP mit einem eigenen Protokoll.
Ein eigener Protokoll auf UDP-Basis kann aus verschiedenen Gründen effizienter sein als TCP. Zum Beispiel kann es einige Pakete als zuverlässig und andere als unzuverlässig kennzeichnen. Daher spielt es keine Rolle, ob ein unzuverlässiges Paket den Empfänger erreicht. Es kann auch mehrere Datenströme verarbeiten, sodass ein verlorenes Paket in einem Stream die anderen Ströme nicht verlangsamt. Zum Beispiel kann es einen Stream für Benutzereingaben und einen weiteren für Chatnachrichten geben. Wenn eine Chatnachricht, die nicht dringend ist, verloren geht, verzögert dies nicht die Eingabe, die kritisch ist. Alternativ kann das eigene Protokoll die Zuverlässigkeit anders umsetzen als TCP, um in Videospielen effizienter zu sein.
Also, wenn TCP so schlecht ist, werden wir dann unser eigenes Transportprotokoll auf UDP-Basis erstellen?
Es ist alles ein wenig komplizierter. Obwohl TCP fast suboptimal für Spielnetzwerksysteme ist, kann es in Ihrem speziellen Spiel gut funktionieren und Ihnen wertvolle Zeit sparen. Zum Beispiel ist Latenz möglicherweise kein Problem für ein rundenbasiertes Spiel oder für Spiele, die nur in LAN-Netzwerken gespielt werden können, wo die Latenz und der Paketverlust viel geringer sind als im Internet.
In vielen erfolgreichen Spielen, darunter World of Warcraft, Minecraft und Terraria, wird TCP verwendet. In den meisten FPS-Spielen kommen jedoch eigene Protokolle auf Basis von UDP zum Einsatz, weshalb wir im Folgenden näher darauf eingehen werden.
Wenn Sie sich entscheiden, TCP zu verwenden, stellen Sie sicher, dass deaktiviert ist, da er Pakete puffert, bevor sie gesendet werden, was die Latenz erhöht.
Um mehr über die Unterschiede zwischen UDP und TCP im Kontext von Mehrspieler-Spielen zu erfahren, können Sie den Artikel von Glenn Fiedler lesen, .
Eigenes Protokoll
Sie möchten ein eigenes Transportprotokoll erstellen, wissen aber nicht, wo Sie anfangen sollen? Sie haben Glück, denn Glenn Fiedler hat dazu zwei großartige Artikel geschrieben. Darin finden Sie viele kluge Gedanken.
Der erste Artikel, Seit 2008 einfacher als das zweite. 2016. Ich empfehle Ihnen, mit einer älteren Version zu beginnen.
Beachten Sie, dass Glenn Fidler ein großer Befürworter des Einsatzes eines eigenen protokollbasierten UDP-Systems ist. Nach dem Lesen seiner Artikel werden Sie sicherlich seine Ansicht übernehmen, dass TCP in Videospielen erhebliche Nachteile hat, und möchten Ihr eigenes Protokoll implementieren.
Wenn Sie jedoch Neuling im Netzwerkbereich sind, machen Sie sich das Leben leichter und verwenden Sie TCP oder eine Bibliothek. Um erfolgreich ein eigenes Transportprotokoll zu implementieren, müssen Sie vorher viel lernen.
Netzwerkbibliotheken
Wenn Sie etwas Effizienteres als TCP benötigen, aber sich nicht mit der Implementierung eines eigenen Protokolls und vielen Details herumschlagen wollen, können Sie auf Netzwerkbibliotheken zurückgreifen. Es gibt viele:
- Glenn Fidler
- , das nicht mehr unterstützt wird, aber sein Fork scheint noch aktiv zu sein.
- ist eine Bibliothek, die für Mehrspieler FPS entwickelt wurde.
- von Valve
Ich habe sie nicht alle ausprobiert, aber ich bevorzuge ENet, da es einfach zu bedienen und zuverlässig ist. Zudem gibt es eine verständliche Dokumentation und ein Tutorial für Anfänger.
Transportprotokoll: Fazit
Zusammenfassend lässt sich sagen, dass es zwei Haupttransportprotokolle gibt: TCP und UDP. TCP bietet zahlreiche nützliche Funktionen: Zuverlässigkeit, Aufrechterhaltung der Paketreihenfolge, Fehlererkennung. UDP hat all das nicht, jedoch hat TCP von Natur aus höhere Latenzen, die für bestimmte Spiele inakzeptabel sein können. Um niedrige Latenzen zu gewährleisten, kann ein eigener Protokoll auf Basis von UDP erstellt oder eine Bibliothek verwendet werden, die ein Transportprotokoll auf UDP-Basis implementiert und für Mehrspieler-Videospiele optimiert ist.
Die Wahl zwischen TCP, UDP und einer Bibliothek hängt von mehreren Faktoren ab. Zunächst von den Bedürfnissen des Spiels: Sind niedrige Latenzen erforderlich? Zweitens, die Anforderungen des Anwendungsprotokolls: Benötigt es ein zuverlässiges Protokoll? Wie im nächsten Abschnitt gezeigt wird, kann ein Anwendungsprotokoll erstellt werden, für das ein unzuverlässiges Protokoll durchaus geeignet ist. Schließlich sollte auch die Erfahrung des Entwicklers der Netzwerkengine berücksichtigt werden.
Ich habe zwei Ratschläge:
- Isolieren Sie das Transportprotokoll so weit wie möglich vom Rest der Anwendung, damit es leicht ersetzt werden kann, ohne den gesamten Code neu schreiben zu müssen.
- Vermeiden Sie vorzeitige Optimierungen. Wenn Sie kein Netzwerkspezialist sind und sich nicht sicher sind, ob Sie ein eigenes Transportprotokoll basierend auf UDP benötigen, können Sie zunächst mit TCP oder Bibliotheken, die Zuverlässigkeit bieten, beginnen und dann testen und messen. Wenn Probleme auftauchen und Sie sicher sind, dass die Ursache im Transportprotokoll liegt, könnte es an der Zeit sein, ein eigenes Transportprotokoll zu entwickeln.
Zum Abschluss dieses Abschnitts empfehle ich Ihnen, " Bryan Hooks, in dem viele der hier besprochenen Themen behandelt werden.
Anwendungsprotokoll
Jetzt, da wir Daten zwischen Clients und Server austauschen können, müssen wir entscheiden, welche Daten übertragen werden sollen und in welchem Format.
Das klassische Modell besteht darin, dass Clients Eingaben oder Aktionen an den Server senden, während der Server den Clients den aktuellen Spielstatus übermittelt.
Der Server sendet keinen vollständigen, sondern einen gefilterten Zustand mit Entitäten, die sich in der Nähe des Spielers befinden. Er tut dies aus drei Gründen. Erstens kann der vollständige Zustand zu groß sein, um ihn häufig zu übertragen. Zweitens interessieren sich die Clients hauptsächlich für visuelle und Audiodaten, da der Großteil der Spielmechanik auf dem Spieleserver simuliert wird. Drittens sollte der Spieler in einigen Spielen bestimmte Daten nicht kennen, wie z.B. die Position des Gegners auf der gegenüberliegenden Seite der Karte, da er sonst Pakete sniffen und genau wissen könnte, wohin er sich bewegen muss, um getroffen zu werden.
Serialisierung
Der erste Schritt besteht darin, die Daten, die wir übertragen möchten (Eingaben oder Spielzustände), in ein geeignetes Übertragungsformat zu konvertieren. Dieser Prozess wird .
genannt. Sofort denkt man an ein menschenlesbares Format wie JSON oder XML. Aber das wäre völlig ineffizient und würde einen Großteil des Bandes unnötig beanspruchen.
Stattdessen wird empfohlen, ein binäres Format zu verwenden, das deutlich kompakter ist. Das bedeutet, dass die Pakete nur einige Bytes enthalten werden. Dabei muss man das Problem berücksichtigen, die auf verschiedenen Computern unterschiedlich sein kann.
Für die Serialisierung von Daten kann man eine Bibliothek verwenden, zum Beispiel:
- von Google
- von Sandstorm
- von Shane Grant und Randolph Vurhis.
Stellen Sie nur sicher, dass die Bibliothek portable Archive erstellt und sich um die Byte-Reihenfolge kümmert.
Eine alternative Lösung könnte eine eigene Implementierung sein, die nicht besonders kompliziert ist, insbesondere wenn Sie in Ihrem Code einen datenorientierten Ansatz verwenden. Außerdem ermöglicht sie Optimierungen, die bei der Verwendung einer Bibliothek nicht immer möglich sind.
Glenn Fiedler hat zwei Artikel über Serialization geschrieben: und .
Kompression
Die Menge an Daten, die zwischen Clients und Server übertragen werden, ist durch die Bandbreite des Kanals begrenzt. Datenkompression ermöglicht es, in jedem Snapshot mehr Daten zu übertragen, die Aktualisierungsfrequenz zu erhöhen oder einfach die Anforderungen an den Kanal zu senken.
Bit-Packaging
Die erste Technik ist das Bit-Packaging. Dabei wird genau die Anzahl von Bits verwendet, die erforderlich ist, um den gewünschten Wert zu beschreiben. Wenn Sie beispielsweise eine Aufzählung haben, die 16 unterschiedliche Werte haben kann, können Sie anstelle eines ganzen Bytes (8 Bits) nur 4 Bits verwenden.
Glenn Fiedler erklärt, wie man dies im zweiten Teil des Artikels umsetzt. .
Bit-Packaging funktioniert besonders gut mit der Diskretisierung, die Thema des nächsten Abschnitts sein wird.
Diskretisierung
ist eine verlustbehaftete Kompressionstechnik, bei der zur Kodierung eines Wertes nur eine Teilmenge der möglichen Werte verwendet wird. Am einfachsten lässt sich Diskretisierung durch das Runden von Gleitkommazahlen umsetzen.
Glenn Fiedler (schon wieder!) zeigt in seinem Artikel, wie man Diskretisierung praktisch anwendet. .
Kompressionsalgorithmen
Die nächste Technik sind verlustfreie Kompressionsalgorithmen.
Hier sind meiner Meinung nach drei der interessantesten Algorithmen, die man kennen sollte:
- mit vorab berechnetem Code, der extrem schnell ist und gute Ergebnisse liefern kann. Er wurde zur Kompression von Paketen in der Netzwerkengine Quake3 verwendet.
- — ein universeller Kompressionsalgorithmus, der niemals die Datenmenge erhöht. Wie man sehen kann, wurde er in einer Vielzahl von Anwendungsbereichen eingesetzt. Für die Statusaktualisierung kann er überflüssig sein. Aber er kann nützlich sein, wenn Sie Assets, lange Texte oder Gelände vom Server an die Kunden versenden müssen.
- — wahrscheinlich der einfachste Kompressionsalgorithmus, aber sehr effektiv für bestimmte Datentypen und kann als Vorverarbeitungsschritt vor zlib verwendet werden. Er eignet sich besonders gut zur Kompression von Gelände, das aus Kacheln oder Voxeln besteht, bei denen viele benachbarte Elemente wiederholt werden.
Delta-Kompression
Die neueste Kompressionstechnik ist die Delta-Kompression. Dabei werden nur die Unterschiede zwischen dem aktuellen Spielzustand und dem letzten Zustand, den der Client erhalten hat, übermittelt.
Erstmals wurde sie im Netzwerk-Engine von Quake3 angewandt. Hier sind zwei Artikel, die erklären, wie sie genutzt wird:
- Bryan Hook
- Fabien Sanglard [ Artikel auf Habré, siehe Abschnitt „Netzwerkmodell“]
Glenn Fiedler hat sie ebenfalls im zweiten Teil seines Artikels verwendet. .
Verschlüsselung
Darüber hinaus könnte es notwendig sein, die Übertragung von Informationen zwischen Clients und Server zu verschlüsseln. Dafür gibt es mehrere Gründe:
- Privatsphäre/Vertraulichkeit: Nachrichten können nur vom Empfänger gelesen werden, und keine andere Person, die das Netzwerk abhört, kann sie lesen.
- Authentifizierung: Die Person, die die Rolle des Spielers übernehmen möchte, muss seinen Schlüssel kennen.
- Betrugsprävention: Es wird für böswillige Spieler viel schwieriger sein, eigene Pakete zum Betrügen zu erstellen; sie müssen das Verschlüsselungsschema reproduzieren und den Schlüssel finden (der sich bei jeder Verbindung ändert).
Ich empfehle dringend, dafür diese Bibliothek zu verwenden. Ich schlage vor, sie zu nutzen , da sie besonders einfach ist und hervorragende Tutorials hat. Besonders interessant ist das Tutorial über , das es ermöglicht, bei jeder neuen Verbindung neue Schlüssel zu generieren.
Anwendungsprotokoll: Fazit
Damit beenden wir das Anwendungsprotokoll. Ich denke, dass Kompression völlig unnötig ist und die Entscheidung darüber hängt nur vom Spiel und der erforderlichen Bandbreite ab. Verschlüsselung halte ich für unverzichtbar, aber im ersten Prototyp kann man darauf verzichten.
Logik der Anwendung
Jetzt sind wir in der Lage, den Zustand im Client zu aktualisieren, aber wir könnten auf Verzögerungsprobleme stoßen. Der Spieler muss warten, um den Spielstatus vom Server zu aktualisieren, um zu sehen, welche Auswirkungen er auf die Welt hatte.
Darüber hinaus ist die Welt zwischen zwei Statusaktualisierungen vollkommen statisch. Wenn die Aktualisierungsrate der Zustände niedrig ist, werden die Bewegungen sehr ruckartig sein.
Es gibt mehrere Techniken, um die Auswirkungen dieses Problems zu verringern, und im nächsten Abschnitt werde ich darüber berichten.
Techniken zur Verzögerungsreduzierung
Alle in diesem Abschnitt beschriebenen Techniken werden in einer Serie ausführlich behandelt von Gabriel Gambetta. Ich empfehle Ihnen dringend, diese großartige Serie von Artikeln zu lesen. Sie enthält auch eine interaktive Demo, die zeigt, wie diese Techniken in der Praxis funktionieren.
Die erste Technik besteht darin, die Eingabewerte direkt zu verwenden, ohne auf eine Antwort vom Server zu warten. Dies wird als Client-seitiges Vorhersagenbezeichnet. Wenn der Client jedoch ein Update vom Server erhält, muss er sicherstellen, dass seine Vorhersage korrekt war. Wenn nicht, muss er einfach seinen Zustand gemäß der vom Server erhaltenen Information ändern, da der Server autoritär ist. Diese Technik wurde erstmals in Quake verwendet. Mehr dazu erfahren Sie in dem Artikel Fabien Sanglard [ auf Habr].
Der zweite Satz von Techniken wird verwendet, um die Bewegung anderer Entitäten zwischen zwei Zustandsaktualisierungen zu glätten. Es gibt zwei Möglichkeiten, dieses Problem zu lösen: Interpolation und Extrapolation. Bei der Interpolation werden die zwei letzten Zustände herangezogen und der Übergang von einem zum anderen angezeigt. Ihr Nachteil ist, dass sie eine gewisse Verzögerung verursacht, da der Client immer sieht, was in der Vergangenheit geschehen ist. Extrapolation hingegen besteht darin, vorherzusagen, wo sich die Entitäten basierend auf dem letzten Zustand befinden sollten, den der Client erhalten hat. Ihr Nachteil ist, dass eine große Abweichung zwischen der Prognose und der tatsächlichen Position auftritt, wenn sich eine Entität vollständig in eine andere Richtung bewegt.
Die letzte, ausgefeilteste Technik, die nur in FPS nützlich ist — das Lag-Kompensation. Bei der Verwendung von Lag-Kompensation berücksichtigt der Server die Verzögerungen des Clients, wenn dieser auf ein Ziel schießt. Wenn ein Spieler beispielsweise einen Kopf-Shot auf seinem Bildschirm erzielt, das Ziel aber aufgrund einer Verzögerung tatsächlich woanders ist, wäre es unfair, dem Spieler das Recht auf den Kill aufgrund der Verzögerung zu verweigern. Daher spult der Server die Zeit zurück auf den Moment, als der Spieler geschossen hat, um zu simulieren, was der Spieler auf seinem Bildschirm gesehen hat, und überprüft die Kollision zwischen seinem Schuss und dem Ziel.
Glenn Fiedler (wie immer!) schrieb 2004 einen Artikel , in dem er die Grundlagen für die Synchronisierung der Physik-Simulation zwischen Server und Client legte. 2014 schrieb er eine neue Artikelreihe , in der er weitere Techniken zur Synchronisierung der Physik-Simulation beschrieb.
Außerdem gibt es in der Valve-Wiki zwei Artikel, und , die sich mit der Kompensation von Verzögerungen befassen.
Betrugsverhinderung
Es gibt zwei Haupttechniken zur Betrugsverhinderung.
Erstens: die Komplexität der Schadenspakete, die von Cheatern gesendet werden. Wie bereits erwähnt, ist eine gute Implementierungsmethode die Verschlüsselung.
Zweitens: Der autoritative Server sollte nur Befehle/Eingaben/Aktionen empfangen. Der Client sollte keine Möglichkeit haben, den Zustand auf dem Server zu verändern, außer durch das Senden von Eingaben. Der Server muss bei jedem Empfang von Eingaben diese vor ihrer Anwendung auf Zulässigkeit überprüfen.
Logik der Anwendung: Fazit
Ich empfehle Ihnen, eine Methode zur Simulation hoher Verzögerungen und niedriger Aktualisierungsraten zu implementieren, um das Verhalten Ihres Spiels unter schlechten Bedingungen zu testen, selbst wenn Client und Server auf demselben Computer ausgeführt werden. Dies erleichtert die Implementierung von Techniken zur Glättung von Verzögerungen erheblich.
Weitere nützliche Ressourcen
Wenn Sie weitere Ressourcen zu Netzwerkmodellen erkunden möchten, finden Sie diese hier:
- — es lohnt sich, seinen gesamten Blog zu lesen, da es viele hervorragende Artikel gibt. alle Artikel zu Netzwerktechnologien sind hier gesammelt.
- von M. Fatih MAR — eine umfassende Liste von Artikeln und Videos über Netzwerktechniken für Videospiele.
- In Es gibt auch viele nützliche Links.
Quelle: habr.com
