Über das Netzwerkmodell in Spielen für Anfänger

Über das Netzwerkmodell in Spielen für Anfänger
In den letzten zwei Wochen habe ich an einer Netzwerk-Engine für mein Spiel gearbeitet. Davor wusste ich überhaupt nichts über Netzwerktechnologien in Spielen, also habe ich viele Artikel gelesen und zahlreiche 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 vor dem Schreiben Ihrer eigenen Spiel-Engine lernen müssen, sowie die besten Ressourcen und Artikel, um diese zu lernen.

Insgesamt gibt es zwei Haupttypen von Netzwerkarchitekturen: Peer-to-Peer und Client-Server. In der Peer-to-Peer-Architektur (p2p) werden Daten zwischen beliebigen Paaren von verbundenen Spielern übertragen, während in der Client-Server-Architektur die Daten nur zwischen den Spielern und dem Server übertragen werden.

Obwohl die Peer-to-Peer-Architektur in einigen Spielen weiterhin verwendet wird, ist die Client-Server-Architektur der Standard: sie ist einfacher zu implementieren, benötigt eine geringere Bandbreite und erleichtert den Schutz gegen Betrug. Daher konzentrieren wir uns in diesem Leitfaden auf die Client-Server-Architektur.

Insbesondere interessieren uns Autoritätsserver: In solchen Systemen hat der Server immer das letzte Wort. Wenn beispielsweise ein Spieler denkt, dass er sich bei den Koordinaten (10, 5) befindet, der Server ihm aber sagt, dass er sich bei (5, 3) befindet, muss der Client seine Position durch die vom Server übertragene ersetzen, und nicht umgekehrt. Der Einsatz von Autoritätsservern erleichtert die Erkennung von Cheatern.

In gaming Netzwerksystemen gibt es drei Hauptkomponenten:

  • Transportprotokoll: wie Daten zwischen Clients und dem 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 verwendet werden, um den Status der Clients und des Servers zu aktualisieren.

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 Server und Clients auszuwählen. Dazu stehen zwei Internetprotokolle zur Verfügung: TCP und UDP. Aber Sie können auch ein eigenes Transportprotokoll auf der Grundlage von einem von diesen erstellen oder eine Bibliothek verwenden, in der sie verwendet werden.

Vergleich von TCP und UDP

Sowohl TCP als auch UDP basieren auf IP. IP ermöglicht die Übertragung von Paketen vom Sender zum Empfänger, garantiert jedoch nicht, dass das gesendete Paket irgendwann beim Empfänger ankommt, dass es ihn mindestens einmal erreicht und dass die Reihenfolge der Pakete korrekt ankommt. Darüber hinaus kann ein Paket nur eine begrenzte Datenmenge enthalten, die durch die Größe definiert wird. MTU.

UDP ist lediglich eine dünne Schicht über IP. Daher hat es die gleichen Einschränkungen. Im Gegensatz dazu bietet TCP viele Funktionen. Es stellt eine zuverlässige, geordnete Verbindung zwischen zwei Knoten mit Fehlerüberprüfung sicher. Daher ist TCP sehr praktisch und wird in vielen anderen Protokollen wie z.B. in HTTP, FTP und SMTP. Aber all diese Funktionen haben ihren Preis: Verzögerung.

Um zu verstehen, warum diese Funktionen Verzögerungen verursachen können, müssen wir klären, wie TCP funktioniert. Wenn der sendende Knoten ein Paket an den empfangenden Knoten sendet, erwartet er eine Bestätigung (ACK). Wenn er nach einer bestimmten Zeit keine erhält (weil das Paket oder die Bestätigung verloren ging oder aus anderen Gründen), wird das Paket erneut gesendet. Darüber hinaus garantiert TCP den Empfang von Paketen in der richtigen Reihenfolge, sodass alle anderen Pakete nicht verarbeitet werden können, bis das verlorene Paket empfangen wurde, selbst wenn sie bereits vom empfangenden Knoten erhalten wurden.

Wie Sie wahrscheinlich verstehen, ist die Verzögerung in Mehrspielerspielen sehr wichtig, insbesondere in so aktiven Genres wie FPS. Genau aus diesem Grund verwenden viele Spiele UDP mit einem eigenen Protokoll.

Ein eigenes auf UDP basierendes Protokoll kann aus verschiedenen Gründen effizienter als TCP sein. Zum Beispiel kann es einige Pakete als zuverlässig und andere als unzuverlässig kennzeichnen. Daher ist es ihm egal, ob das unzuverlässige Paket den Empfänger erreicht hat. Oder es kann mehrere Datenströme verarbeiten, sodass ein Paket, das in einem Stream verloren geht, die anderen Streams nicht verlangsamt. Zum Beispiel kann es einen Stream für die Eingabe des Spielers und einen weiteren Stream für Chat-Nachrichten geben. Wenn eine Chat-Nachricht, die keine dringenden Daten sind, verloren geht, verlangsamt sie nicht die Eingabe, die dringend ist. Alternativ kann das eigene Protokoll Zuverlässigkeit anders umsetzen als TCP, um in Videospielen effektiver zu sein.

Wenn TCP so schlecht ist, wollen wir dann unser eigenes Transportprotokoll auf UDP-Basis erstellen?

Alles ist ein wenig komplizierter. Auch wenn TCP fast suboptimal für spielbasierte Netzwerksysteme ist, kann es in Ihrem spezifischen Spiel recht gut funktionieren und Ihnen wertvolle Zeit sparen. Zum Beispiel könnte Latenz für ein rundenbasiertes Spiel oder für ein Spiel, das nur in LAN-Netzwerken gespielt werden kann, kein Problem darstellen, wo Latenzen und Paketverluste viel geringer sind als im Internet.

In vielen erfolgreichen Spielen, einschließlich World of Warcraft, Minecraft und Terraria, wird TCP verwendet. Allerdings verwenden die meisten FPS eigene Protokolle auf UDP-Basis, deshalb werden wir sie unten genauer besprechen.

Wenn Sie sich entscheiden, TCP zu verwenden, achten Sie darauf, dass das Nagle-Algorithmusdeaktiviert ist, da dieser Pakete vor dem Senden puffert und somit die Latenz erhöht.

Um mehr über die Unterschiede zwischen UDP und TCP im Kontext von Mehrspielerspielen zu erfahren, können Sie den Artikel von Glenn Fiedler lesen. UDP vs. TCP.

Eigenes Protokoll

Also, Sie möchten ein eigenes Transportprotokoll erstellen, wissen aber nicht, wo Sie anfangen sollen? Glück gehabt, denn Glenn Fiedler hat dazu zwei großartige Artikel geschrieben. In ihnen finden Sie viele clevere Gedanken.

Der erste Artikel, Networking for Game Programmers aus dem Jahr 2008 ist einfacher als der zweite, Building A Game Network Protocol von 2016. Ich empfehle Ihnen, mit dem älteren zu beginnen.

Bedenken Sie, dass Glenn Fiedler ein großer Befürworter der Verwendung eines eigenen Protokolls auf UDP-Basis ist. Nach dem Lesen seiner Artikel werden Sie sicher seine Meinung übernehmen, dass TCP in Videospielen erhebliche Nachteile hat, und möchten Ihr eigenes Protokoll implementieren.

Aber wenn Sie neu in der Netzwerkarbeit sind, gönnen Sie sich und verwenden Sie TCP oder eine Bibliothek. Um erfolgreich ein eigenes Transportprotokoll zu implementieren, müssen Sie vorher viel lernen.

Netzwerklibraries

Wenn Sie etwas Effizienteres als TCP benötigen, aber sich nicht um die Implementierung eines eigenen Protokolls kümmern möchten und in viele Einzelheiten eintauchen wollen, können Sie auf eine Netzwerklibrary zurückgreifen. Es gibt viele davon:

Ich habe sie nicht alle ausprobiert, gebe aber ENet den Vorzug, weil sie einfach zu bedienen und zuverlässig ist. Darüber hinaus gibt es eine verständliche Dokumentation und Tutorials für Anfänger.

Transportprotokoll: Fazit

Zusammenfassend gibt es zwei Haupttransportprotokolle: TCP und UDP. TCP hat viele nützliche Eigenschaften: Zuverlässigkeit, Erhaltung der Paketreihenfolge, Fehlererkennung. UDP hat all dies nicht, aber TCP hat aufgrund seiner Natur erhöhte Latenzen, die für einige Spiele unzulässig sind. Das heißt, um niedrige Latenzen zu gewährleisten, kann man ein eigenes Protokoll auf Basis von UDP erstellen oder eine Bibliothek verwenden, die ein transportprotokoll auf UDP implementiert und für Multiplayer-Videospiele angepasst ist.

Die Wahl zwischen TCP, UDP und einer Bibliothek hängt von mehreren Faktoren ab. Erstens von den Anforderungen des Spiels: benötigt es niedrige Latenzen? Zweitens von den Anforderungen des Anwendungsprotokolls: benötigt es ein zuverlässiges Protokoll? Wie wir im nächsten Abschnitt sehen werden, kann man ein Anwendungsprotokoll erstellen, für das ein unzuverlässiges Protokoll durchaus geeignet ist. Schließlich muss auch die Erfahrung des Entwicklers des Netzwerk-Engines berücksichtigt werden.

Ich habe zwei Ratschläge:

  • Maximal abstrahieren Sie das Transportprotokoll von dem Rest der Anwendung, damit es leicht ersetzt werden kann, ohne den gesamten Code neu zu schreiben.
  • Vermeiden Sie voreilige Optimierungen. Wenn Sie kein Netzwerkspezialist sind und unsicher sind, ob Sie ein eigenes Transportprotokoll auf Basis von UDP benötigen, können Sie mit TCP oder einer Bibliothek beginnen, die Zuverlässigkeit bietet, und dann die Leistung testen und messen. Wenn Probleme auftreten und Sie sicher sind, dass die Ursache im Transportprotokoll liegt, könnte es an der Zeit sein, ein eigenes Transportprotokoll zu entwickeln.

Abschließend empfehle ich Ihnen, zu lesen Introduction to Multiplayer Game Programming von Brian Hook, in dem viele der hier diskutierten 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 Schema besteht darin, dass die Clients Eingaben oder Aktionen an den Server senden, während der Server den Clients den aktuellen Spielzustand sendet.

Der Server sendet nicht den vollständigen, sondern einen gefilterten Zustand mit den Entitäten, die sich in der Nähe des Spielers befinden. Er macht dies aus drei Gründen. Erstens kann der vollständige Zustand zu groß sein, um ihn mit hoher Frequenz zu übertragen. Zweitens interessieren sich die Clients hauptsächlich für visuelle und Audiodaten, da der Großteil der Spiel-Logik auf dem Spiel-Server simuliert wird. Drittens darf der Spieler in einigen Spielen bestimmte Daten nicht kennen, zum Beispiel die Position des Gegners am anderen Ende der Karte, da er sonst die Pakete schnüffeln und genau wissen könnte, wohin er sich bewegen muss, um ihn zu töten.

Serialisierung

Der erste Schritt besteht darin, die Daten, die wir senden möchten (Eingaben oder Spielzustand), in ein übertragungsfähiges Format umzuwandeln. Dieser Prozess wird Serialisierung.

genannt. Sofort fällt einem ein, ein menschenlesbares Format wie JSON oder XML zu verwenden. Doch das wäre völlig ineffizient und würde einen Großteil der Bandbreite verschwenden.

Stattdessen wird empfohlen, ein binäres Format zu verwenden, das wesentlich kompakter ist. Das heißt, die Pakete würden nur wenige Bytes enthalten. Hierbei muss das Problem der Byte-Reihenfolgeberücksichtigt werden, da diese auf verschiedenen Computern unterschiedlich sein kann.

Für die Serialisierung der Daten kann eine Bibliothek verwendet werden, beispielsweise:

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 Eigenentwicklung sein, die nicht besonders kompliziert ist, insbesondere wenn im Code ein datenzentrierter Ansatz verwendet wird. Darüber hinaus ermöglicht es Ihnen, Optimierungen vorzunehmen, die bei der Verwendung einer Bibliothek nicht immer möglich sind.

Glenn Fiedler hat über Serialisierung zwei Artikel geschrieben: Reading and Writing Packets und Serialization Strategies.

Kompression

Die Menge an Daten, die zwischen Clients und Server übertragen werden, ist durch die Bandbreite des Kanals begrenzt. Die Datenkompression ermöglicht es, in jedem Snapshot mehr Daten zu übertragen, die Aktualisierungsrate zu erhöhen oder einfach die Anforderungen an den Kanal zu senken.

Bit-Packing

Die erste Technik ist die Bit-Packing. Dabei wird genau die Anzahl von Bits verwendet, die benötigt wird, um die erforderliche Größe zu beschreiben. Zum Beispiel, wenn Sie ein Enumeration haben, das 16 verschiedene Werte haben kann, können Sie anstelle eines gesamten Bytes (8 Bits) nur 4 Bits verwenden.

Glenn Fielder erklärt, wie man dies umsetzt, im zweiten Teil des Artikels. Reading and Writing Packets.

Bit-Packing funktioniert besonders gut mit der Diskretisierung, die das Thema des nächsten Abschnitts sein wird.

Diskretisierung

Diskretisierung ist eine verlustbehaftete Kompressionstechnik, die darin besteht, lediglich eine Teilmenge möglicher Werte zur Kodierung der Größe zu verwenden. Am einfachsten ist es, Diskretisierung durch Rundung von Fließkommazahlen zu realisieren.

Glenn Fielder (schon wieder!) zeigt, wie man Diskretisierung in der Praxis anwendet, in seinem Artikel Snapshot-Kompression.

Kompressionsalgorithmen

Die nächste Technik wird verlustfreie Kompressionsalgorithmen sein.

Hier sind meiner Meinung nach die drei interessantesten Algorithmen, die man kennen sollte:

  • Huffman-Kodierung mit vorab berechnetem Code, der extrem schnell ist und gute Ergebnisse liefern kann. Er wurde zur Komprimierung von Paketen in der Netzwerk-Engine Quake3 verwendet.
  • zlib ist ein algorithmus zur allgemeinen Datenkompression, der die Datenmenge nie erhöht. Wie man sehen kann hier, wurde er in vielen Anwendungsbereichen eingesetzt. Bei der Aktualisierung von Zuständen kann er überflüssig sein. Aber er kann nützlich sein, wenn Sie Assets, lange Texte oder Höheninformationen vom Server an die Clients senden müssen.
  • Längenkopie ist wahrscheinlich der einfachste Kompressionsalgorithmus, aber er ist sehr effektiv für bestimmte Datentypen und kann als Vorverarbeitungsschritt vor zlib verwendet werden. Er eignet sich besonders gut zur Kompression von Höheninformationen, die aus Kacheln oder Voxeln bestehen, bei denen viele benachbarte Elemente wiederholt werden.

Delta-Kompression

Die letzte Kompressionsmethode 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 in der Netzwerk-Engine Quake3 angewendet. Hier sind zwei Artikel, die erklären, wie sie verwendet wird:

Glen Fiedler hat sie auch im zweiten Teil seines Artikels verwendet. Snapshot-Kompression.

Verschlüsselung

Außerdem kann 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 kein anderes, das im Netzwerk schnappt, kann sie lesen.
  • Authentifizierung: Eine Person, die die Rolle des Spielers übernehmen möchte, muss seinen Schlüssel kennen.
  • Betrug verhindern: Es wird für böswillige Spieler viel schwieriger, eigene Pakete zum Betrügen zu erstellen, da sie das Verschlüsselungsschema reproduzieren und den Schlüssel finden müssen (der sich bei jeder Verbindung ändert).

Ich empfehle dringend, dafür eine Bibliothek zu verwenden. Ich schlage vor, libsodium, da sie besonders einfach zu benutzen ist und hervorragende Tutorials bietet. Besonders interessant ist das Tutorial über Schlüsselweitergabe, das es ermöglicht, bei jeder neuen Verbindung neue Schlüssel zu generieren.

Anwendungsprotokoll: Schlussfolgerung

Damit beenden wir das Anwendungsprotokoll. Ich denke, dass Kompression völlig optional ist und die Entscheidung, ob sie verwendet wird, nur vom Spiel und der erforderlichen Bandbreite abhängt. Verschlüsselung ist meiner Meinung nach zwingend erforderlich, kann jedoch im ersten Prototypen weggelassen werden.

Logik der Anwendung

Jetzt sind wir in der Lage, den Status im Client zu aktualisieren, können aber auf Verzögerungsprobleme stoßen. Der Spieler muss nach der Eingabe auf das Statusupdate vom Server warten, um zu sehen, welche Auswirkungen er auf die Welt hatte.

Darüber hinaus bleibt die Welt zwischen zwei Statusupdates völlig statisch. Wenn die Aktualisierungsfrequenz 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 sprechen.

Techniken zur Milderung von Verzögerungen

Alle in diesem Abschnitt beschriebenen Techniken sind ausführlich in der Reihe Fast-Paced Multiplayer von Gabriel Gambetta behandelt. Ich empfehle dringend, diese großartige Artikelreihe zu lesen. Sie enthält auch eine interaktive Demo, die zeigt, wie diese Techniken in der Praxis funktionieren.

Die erste Technik besteht darin, das Eingaberesultat direkt anzuwenden, ohne auf eine Antwort vom Server zu warten. Dies wird Client-seitige Vorhersage. Allerdings, wenn der Client ein Update vom Server erhält, muss er sicherstellen, dass seine Vorhersage korrekt war. Wenn dies nicht der Fall ist, muss er einfach seinen Zustand entsprechend dem vom Server erhaltenen ändern, da der Server autoritär ist. Diese Technik wurde erstmals in Quake verwendet. Mehr dazu finden Sie in dem Artikel Quake Engine Code-Überprüfung von Fabien Sanglard [Übersetzung auf Habr].

Ein zweites Set von Techniken wird verwendet, um die Bewegung anderer Entitäten zwischen zwei Statusaktualisierungen zu glätten. Es gibt zwei Möglichkeiten, dieses Problem zu lösen: Interpolation und Extrapolation. Im Fall der Interpolation werden die letzten beiden Zustände herangezogen und der Übergang von einem zum anderen angezeigt. Ihr Nachteil besteht darin, dass sie eine kleine Verzögerung verursacht, da der Client immer sieht, was in der Vergangenheit passiert ist. Extrapolation besteht darin, vorherzusagen, wo sich Entitäten jetzt basierend auf dem letzten Zustand befinden sollten, den der Client erhalten hat. Ihr Nachteil ist, dass, wenn eine Entität die Bewegungsrichtung vollständig ändert, eine große Abweichung zwischen der Vorhersage und der tatsächlichen Position entsteht.

Die letzte, fortschrittlichste Technik, die nur in FPS nützlich ist, ist die Lag-Compensation. Bei der Verwendung von Lag-Compensation berücksichtigt der Server die Verzögerungen des Clients, wenn er auf ein Ziel schießt. Wenn ein Spieler zum Beispiel einen Kopfschuss auf seinem Bildschirm ausgeführt hat, sich sein Ziel aufgrund der Verzögerung aber tatsächlich woanders befand, wäre es unfair, dem Spieler das Recht auf einen Kill aufgrund der Verzögerung zu verweigern. Daher spult der Server die Zeit zurück zu dem Moment, als der Spieler geschossen hat, um zu simulieren, was der Spieler auf seinem Bildschirm gesehen hat, und prüft die Kollision zwischen seinem Schuss und dem Ziel.

Glenn Fiedler (wie immer!) schrieb 2004 einen Artikel Network Physics (2004), in dem er das Fundament für die Synchronisation der Physiksimulation zwischen Server und Client legte. 2014 schrieb er eine neue Artikelserie Networking Physics, in der er weitere Techniken zur Synchronisation der Physiksimulation beschrieb.

Im Wiki des Unternehmens Valve gibt es auch zwei Artikel, Source Multiplayer Networking und Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization , die die Kompensation von Verzögerungen behandeln.

Betrugsbekämpfung

Es gibt zwei grundlegende Techniken zur Betrugsbekämpfung.

Erstens: die Komplexität des Versands von schädlichen Paketen durch Cheater. Wie bereits erwähnt, ist eine gute Möglichkeit, dies zu implementieren, die Verschlüsselung.

Zweitens: der autoritäre Server sollte nur Befehle/Eingaben/Handlungen erhalten. Der Client sollte nicht in der Lage sein, den Zustand auf dem Server zu ändern, außer durch das Senden von Eingaben. Der Server sollte bei jedem Eingang von Eingaben prüfen, ob diese zulässig sind, bevor er sie anwendet.

Logik der Anwendung: Schlussfolgerung

Ich empfehle Ihnen, eine Methode zur Simulation großer Verzögerungen und niedriger Aktualisierungsfrequenzen zu implementieren, um das Verhalten Ihres Spiels unter schlechten Bedingungen zu testen, selbst wenn Client und Server auf demselben Computer laufen. Dies vereinfacht die Implementierung von Verzögerungsglättungs-Techniken erheblich.

Weitere nützliche Ressourcen

Wenn Sie weitere Ressourcen zu Netzmodellen erkunden möchten, finden Sie diese hier:

Quelle: habr.com

60GB SSD 8Gb DDR4