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

Bild

In den letzten zwei Wochen habe ich an einer Netzwerk-Engine für mein Spiel gearbeitet. Vorher wusste ich nichts über Netzwerktechnologien in Spielen, deshalb las ich viele Artikel und führte viele Experimente durch, um alle Konzepte zu verstehen und meine eigene Netzwerk-Engine schreiben zu können.

In diesem Handbuch möchte ich Ihnen verschiedene Konzepte vorstellen, die Sie lernen müssen, bevor Sie Ihre eigene Spiel-Engine schreiben, sowie die besten Ressourcen und Artikel, um sie zu lernen.

Im Allgemeinen gibt es zwei Haupttypen von Netzwerkarchitekturen: Peer-to-Peer und Client-Server. In der Peer-to-Peer-Architektur (p2p) werden Daten zwischen einem beliebigen Paar verbundener Player übertragen, und in der Client-Server-Architektur werden Daten nur zwischen den Playern und dem Server übertragen.

Obwohl die Peer-to-Peer-Architektur in einigen Spielen immer noch verwendet wird, ist der Standard Client-Server: Sie ist einfacher zu implementieren, erfordert eine geringere Kanalbreite und erleichtert den Schutz vor Betrug. Daher konzentrieren wir uns in diesem Handbuch auf die Client-Server-Architektur.

Insbesondere sind wir am meisten an autoritären Servern interessiert: In solchen Systemen hat der Server immer Recht. Wenn ein Spieler beispielsweise denkt, dass er sich in Koordinaten befindet (10, 5) und der Server ihm mitteilt, dass er sich in Koordinaten befindet (5, 3), sollte der Client seine Position durch die vom Server übertragene Position ersetzen und nicht umgekehrt. Die Verwendung autoritärer Server erleichtert die Erkennung von Betrügern.

Gaming-Netzwerksysteme bestehen aus drei Hauptkomponenten:

  • Transportprotokoll: Wie Daten zwischen Clients und Server übertragen werden.
  • Anwendungsprotokoll: Was wird von Clients zum Server und vom Server zu Clients übertragen und in welchem ​​Format.
  • Anwendungslogik: Wie übertragene Daten verwendet werden, um den Status von Clients und Servern zu aktualisieren.

Es ist sehr wichtig, die Rolle jedes Teils und die damit verbundenen Schwierigkeiten zu verstehen.

Transportprotokoll


Der erste Schritt besteht darin, ein Protokoll für den Datentransport zwischen Server und Clients auszuwählen. Hierfür gibt es zwei Internetprotokolle: TCP und UDP . Sie können jedoch Ihr eigenes Transportprotokoll basierend auf einem von ihnen erstellen oder die Bibliothek verwenden, in der sie verwendet werden.

Vergleich von TCP und UDP


Sowohl TCP als auch UDP sind IP- basiert. IP ermöglicht es Ihnen, ein Paket von der Quelle zum Empfänger zu übertragen, garantiert jedoch nicht, dass das gesendete Paket früher oder später den Empfänger erreicht, dass es mindestens einmal erreicht wird und dass die Reihenfolge der Pakete in der richtigen Reihenfolge ankommt. Darüber hinaus kann ein Paket nur eine begrenzte Datengröße enthalten, die durch den MTU- Wert angegeben wird.

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

Um zu verstehen, warum diese Funktionen zu Verzögerungen führen können, müssen Sie wissen, wie TCP funktioniert. Wenn der sendende Knoten das Paket an den empfangenden Knoten weiterleitet, erwartet er eine Bestätigung (ACK). Wenn er es nach einer bestimmten Zeit nicht erhält (weil das Paket oder die Bestätigung verloren gegangen ist oder aus anderen Gründen), sendet er das Paket erneut. Darüber hinaus stellt TCP sicher, dass Pakete in der richtigen Reihenfolge empfangen werden. Daher können bis zum Empfang eines verlorenen Pakets alle anderen Pakete nicht verarbeitet werden, selbst wenn sie bereits vom empfangenden Knoten empfangen wurden.

Aber wie Sie wahrscheinlich verstehen, ist die Verzögerung bei Multiplayer-Spielen sehr wichtig, insbesondere bei aktiven Genres wie FPS. Aus diesem Grund verwenden viele Spiele UDP mit einem eigenen Protokoll.

Ein natives UDP-basiertes Protokoll kann aus verschiedenen Gründen effizienter sein als TCP. Beispielsweise können einige Pakete als vertrauenswürdig und andere als nicht vertrauenswürdig markiert werden. Daher ist es ihm egal, ob das nicht vertrauenswürdige Paket den Empfänger erreicht hat. Oder es können mehrere Datenströme verarbeitet werden, sodass ein in einem Stream verlorenes Paket die verbleibenden Streams nicht verlangsamt. Beispielsweise kann es einen Stream für Spielereingaben und einen anderen Stream für Chat-Nachrichten geben. Wenn eine Chat-Nachricht verloren geht, bei der es sich nicht um dringende Daten handelt, wird die dringende Eingabe nicht verlangsamt. Oder ein proprietäres Protokoll kann die Zuverlässigkeit anders als in TCP implementieren, um in Videospielen effizienter zu sein.

Also, wenn TCP so beschissen ist, werden wir unser eigenes Transportprotokoll basierend auf UDP erstellen?

Alles ist etwas komplizierter. Obwohl TCP für Gaming-Netzwerksysteme fast nicht optimal ist, kann es in Ihrem Spiel einwandfrei funktionieren und wertvolle Zeit sparen. Beispielsweise ist Verzögerung möglicherweise kein Problem für ein rundenbasiertes Spiel oder ein Spiel, das nur in LANs gespielt werden kann, in denen es viel weniger Verzögerungen und Paketverluste gibt als im Internet.

Viele erfolgreiche Spiele, darunter World of Warcraft, Minecraft und Terraria, verwenden TCP. Die meisten FPS verwenden jedoch proprietäre UDP-basierte Protokolle, daher werden wir im Folgenden näher darauf eingehen.

Wenn Sie sich für die Verwendung von TCP entscheiden, stellen Sie sicher, dass der Nagle-Algorithmus deaktiviert ist, da er Pakete vor dem Senden puffert, was bedeutet, dass die Verzögerung erhöht wird.

Um mehr über die Unterschiede zwischen UDP und TCP im Zusammenhang mit Multiplayer-Spielen zu erfahren, lesen Sie den Artikel von Glenn Fiedler UDP vs. TCP

Eigenes Protokoll


Sie möchten also Ihr eigenes Transportprotokoll erstellen, wissen aber nicht, wo Sie anfangen sollen? Sie haben Glück, denn Glenn Fiedler hat zwei erstaunliche Artikel darüber geschrieben. Sie werden viele kluge Gedanken in ihnen finden.

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

Denken Sie daran, dass Glenn Fiedler ein großer Befürworter der Verwendung seines eigenen UDP-Protokolls ist. Und nachdem Sie seine Artikel gelesen haben, werden Sie sicherlich über seine Meinung hinwegkommen, dass TCP schwerwiegende Mängel bei Videospielen aufweist und Sie Ihr eigenes Protokoll implementieren möchten.

Wenn Sie neu im Networking sind, tun Sie sich selbst einen Gefallen und verwenden Sie TCP oder eine Bibliothek. Um Ihr eigenes Transportprotokoll erfolgreich zu implementieren, müssen Sie zunächst viel lernen.

Netzwerkbibliotheken


Wenn Sie etwas Effizienteres als TCP benötigen, sich aber nicht mit der Implementierung Ihres eigenen Protokolls befassen und auf viele Details eingehen möchten, können Sie die Netzwerkbibliothek verwenden. Es gibt viele von ihnen:


Ich habe nicht alle ausprobiert, aber ich bevorzuge ENet, weil es einfach zu bedienen und zuverlässig ist. Darüber hinaus verfügt sie über eine klare Dokumentation und ein Tutorial für Anfänger.

Transportprotokoll: Schlussfolgerung


Zusammenfassend: Es gibt zwei Haupttransportprotokolle: TCP und UDP. TCP bietet viele nützliche Funktionen: Zuverlässigkeit, Paketreihenfolge, Fehlererkennung. UDP hat nicht all dies, aber TCP hat naturgemäß vermehrt Verzögerungen, die für einige Spiele nicht akzeptabel sind. Um niedrige Latenzen zu gewährleisten, können Sie Ihr eigenes UDP-basiertes Protokoll erstellen oder eine Bibliothek verwenden, die das UDP-Transportprotokoll implementiert und für Videospiele für mehrere Spieler geeignet ist.

Die Wahl zwischen TCP, UDP und der Bibliothek hängt von mehreren Faktoren ab. Erstens aus den Anforderungen des Spiels: Benötigt es geringe Latenzen? Zweitens aus den Anforderungen des Anwendungsprotokolls: Benötigt es ein zuverlässiges Protokoll? Wie wir im nächsten Teil sehen werden, können Sie ein Anwendungsprotokoll erstellen, für das ein unzuverlässiges Protokoll gut geeignet ist. Schließlich müssen Sie noch die Erfahrung des Entwicklers der Netzwerk-Engine berücksichtigen.

Ich habe zwei Tipps:

  • Maximieren Sie das Transportprotokoll aus dem Rest der Anwendung, damit es problemlos ersetzt werden kann, ohne den gesamten Code neu zu schreiben.
  • Führen Sie keine vorzeitige Optimierung durch. Wenn Sie kein Netzwerkspezialist sind und nicht sicher sind, ob Sie ein eigenes UDP-basiertes Transportprotokoll benötigen, können Sie mit TCP oder einer Bibliothek beginnen, die Zuverlässigkeit bietet, und dann die Leistung testen und messen. Wenn Sie Probleme haben und sicher sind, dass der Grund im Transportprotokoll liegt, ist es möglicherweise an der Zeit, Ihr eigenes Transportprotokoll zu erstellen.

Am Ende dieses Teils empfehle ich Ihnen, Brian Hooks Einführung in die Multiplayer - Spielprogrammierung zu lesen, die viele der hier behandelten Themen abdeckt.

Anwendungsprotokoll


Jetzt, da wir Daten zwischen Clients und dem Server austauschen können, müssen wir entscheiden, welche Daten in welchem ​​Format übertragen werden sollen.

Das klassische Schema besteht darin, dass Clients Eingaben oder Aktionen an den Server senden und der Server den aktuellen Spielstatus an Clients sendet.

Der Server sendet keinen vollständigen, sondern gefilterten Status mit Entitäten, die sich neben dem Player befinden. Er tut dies aus drei Gründen. Erstens kann der Gesamtzustand für eine Hochfrequenzübertragung zu groß sein. Zweitens interessieren sich Kunden hauptsächlich für visuelle und akustische Daten, da der größte Teil der Spielelogik auf dem Spieleserver simuliert wird. Drittens muss der Spieler in einigen Spielen bestimmte Daten nicht kennen, beispielsweise die Position des Gegners am anderen Ende der Karte, da er sonst Pakete schnüffeln und genau wissen kann, wohin er sich bewegen muss, um ihn zu töten.

Serialisierung


Der erste Schritt besteht darin, die zu sendenden Daten (Eingabe oder Spielstatus) in ein für die Übertragung geeignetes Format zu konvertieren. Dieser Vorgang wird als Serialisierung bezeichnet .

Der Gedanke kommt sofort in den Sinn, ein für Menschen lesbares Format wie JSON oder XML zu verwenden. Aber es wird völlig unwirksam sein und vergebens den größten Teil des Kanals einnehmen.

Stattdessen wird empfohlen, ein viel kompakteres Binärformat zu verwenden. Das heißt, Pakete enthalten nur wenige Bytes. Hier müssen Sie das Problem der Bytereihenfolge berücksichtigen, das auf verschiedenen Computern unterschiedlich sein kann.

Sie können eine Bibliothek zum Serialisieren von Daten verwenden, zum Beispiel:


Stellen Sie einfach sicher, dass die Bibliothek tragbare Archive erstellt und sich um die Bytereihenfolge kümmert.

Eine unabhängige Lösung kann eine unabhängige Implementierung sein. Sie ist nicht besonders kompliziert, insbesondere wenn Sie im Code einen datenorientierten Ansatz verwenden. Darüber hinaus können Sie Optimierungen durchführen, die bei Verwendung der Bibliothek nicht immer möglich sind.

Glenn Fiedler hat zwei Artikel zur Serialisierung geschrieben: Lesen und Schreiben von Paketen und Serialisierungsstrategien .

Komprimierung


Die zwischen Clients und Server übertragene Datenmenge wird durch die Bandbreite des Kanals begrenzt. Mit der Datenkomprimierung können Sie mehr Daten in jedem Snapshot übertragen, die Aktualisierungsrate erhöhen oder einfach die Kanalanforderungen reduzieren.

Bitverpackung


Die erste Technik ist das Bitpacken. Es besteht darin, genau die Anzahl der Bits zu verwenden, die zur Beschreibung des gewünschten Werts erforderlich sind. Wenn Sie beispielsweise eine Aufzählung mit 16 verschiedenen Werten haben, können Sie anstelle eines ganzen Bytes (8 Bit) nur 4 Bit verwenden.

Glenn Fiedler erklärt im zweiten Teil des Artikels über das Lesen und Schreiben von Paketen , wie dies implementiert wird.

Das Packen von Bits eignet sich besonders gut für die Abtastung, die im nächsten Abschnitt behandelt wird.

Diskretisierung


Die Diskretisierung ist eine verlustbehaftete Komprimierungstechnik, bei der nur eine Teilmenge der möglichen Werte zum Codieren eines Werts verwendet wird. Der einfachste Weg, die Diskretisierung zu implementieren, ist das Abrunden von Gleitkommazahlen.

Glenn Fiedler (wieder!) Zeigt in seinem Artikel zur Snapshot-Komprimierung, wie Sampling in der Praxis angewendet wird .

Komprimierungsalgorithmen


Die nächste Technik werden verlustfreie Komprimierungsalgorithmen sein.

Hier sind meiner Meinung nach die drei interessantesten Algorithmen, die Sie kennen müssen:

  • Huffman-Codierung mit vorberechnetem Code, der extrem schnell ist und gute Ergebnisse liefern kann. Es wurde verwendet, um Pakete in der Quake3-Netzwerk-Engine zu komprimieren.
  • zlib ist ein universeller Komprimierungsalgorithmus, der die Datenmenge niemals erhöht. Wie hier zu sehen ist , wurde es in vielen Anwendungen verwendet. Das Aktualisieren von Status kann redundant sein. Dies kann jedoch nützlich sein, wenn Sie Assets, Langtexte oder Erleichterungen vom Server an Clients senden müssen.
  • Das Kopieren von Serienlängen ist wahrscheinlich der einfachste Komprimierungsalgorithmus, aber für bestimmte Datentypen sehr effektiv und kann als Vorverarbeitungsschritt vor zlib verwendet werden. Es eignet sich besonders zum Komprimieren von Gelände aus Kacheln oder Voxeln, in denen sich viele benachbarte Elemente wiederholen.

Delta-Komprimierung


Die neueste Komprimierungstechnik ist die Delta-Komprimierung. Es liegt in der Tatsache, dass nur Unterschiede zwischen dem aktuellen Spielstatus und dem letzten vom Client empfangenen Status übertragen werden.

Es wurde erstmals in der Quake3-Netzwerk-Engine verwendet. Hier sind zwei Artikel, die erklären, wie man es benutzt:


Glenn Fiedler verwendete es auch im zweiten Teil seines Artikels zur Snapshot-Komprimierung .

Verschlüsselung


Darüber hinaus müssen Sie möglicherweise die Übertragung von Informationen zwischen Clients und dem Server verschlüsseln. Dafür gibt es mehrere Gründe:

  • Datenschutz / Vertraulichkeit: Nachrichten können nur vom Empfänger gelesen werden, und keine andere Person, die am Netzwerk schnüffelt, kann sie lesen.
  • Authentifizierung: Eine Person, die die Rolle eines Spielers spielen möchte, muss ihren Schlüssel kennen.
  • Betrugsprävention: Es wird für böswillige Spieler viel schwieriger sein, ihre eigenen Betrugspakete zu erstellen. Sie müssen das Verschlüsselungsschema reproduzieren und den Schlüssel finden (der sich mit jeder Verbindung ändert).

Ich empfehle dringend, die Bibliothek dafür zu verwenden. Ich schlage vor, libsodium zu verwenden, da es besonders einfach ist und großartige Tutorials enthält. Von besonderem Interesse ist das Tutorial zum Schlüsselaustausch , mit dem Sie bei jeder neuen Verbindung neue Schlüssel generieren können.

Anwendungsprotokoll: Schlussfolgerung


Wir werden mit dem Anwendungsprotokoll enden. Ich glaube, dass die Komprimierung völlig optional ist und die Entscheidung, sie zu verwenden, nur vom Spiel und der erforderlichen Bandbreite abhängt. Die Verschlüsselung ist meiner Meinung nach obligatorisch, aber im ersten Prototyp können Sie darauf verzichten.

Anwendungslogik


Jetzt können wir den Status im Client aktualisieren, es können jedoch Probleme mit Verzögerungen auftreten. Nach der Eingabe muss der Spieler auf die Aktualisierung des Spielstatus vom Server warten, um zu sehen, welche Auswirkungen er auf die Welt hatte.

Darüber hinaus ist die Welt zwischen zwei Statusaktualisierungen vollständig statisch. Wenn die Bildwiederholfrequenz der Zustände niedrig ist, sind die Bewegungen sehr zuckend.

Es gibt verschiedene Techniken, um die Auswirkungen dieses Problems zu verringern, und im nächsten Abschnitt werde ich darüber sprechen.

Glättungstechniken verzögern


Alle in diesem Abschnitt beschriebenen Techniken werden in der Fast-Paced Multiplayer- Reihe von Gabriel Gambetta ausführlich beschrieben. Ich empfehle dringend, diese großartige Artikelserie zu lesen. Es gibt auch eine interaktive Demo, mit der Sie sehen können, wie diese Techniken in der Praxis funktionieren.

Die erste Technik besteht darin, die Eingabe direkt anzuwenden, ohne auf eine Antwort vom Server zu warten. Dies wird als clientseitige Vorhersage bezeichnet . Wenn der Client das Update jedoch vom Server erhält, muss er sicherstellen, dass seine Prognose korrekt war. Wenn dies nicht der Fall ist, muss er nur seinen Status entsprechend dem vom Server empfangenen ändern, da der Server autoritär ist. Diese Technik wurde erstmals bei Quake angewendet. Weitere Informationen finden Sie im Artikel Quake Engine Code Review von Fabien Sanglar [ Übersetzung in Habré].

Der zweite Satz von Techniken wird verwendet, um die Bewegung anderer Entitäten zwischen zwei Statusaktualisierungen zu glätten. Es gibt zwei Möglichkeiten, um dieses Problem zu lösen: Interpolation und Extrapolation. Bei der Interpolation werden die letzten beiden Zustände genommen und der Übergang von einem zum anderen gezeigt. Der Nachteil ist, dass es einen kleinen Teil der Verzögerung verursacht, da der Client immer sieht, was in der Vergangenheit passiert ist. Die Extrapolation sagt voraus, wo Entitäten jetzt basierend auf dem letzten vom Client empfangenen Status basieren sollen. Sein Nachteil ist, dass, wenn die Entität die Bewegungsrichtung vollständig ändert, ein großer Fehler zwischen der Prognose und der realen Position auftritt.

Die letzte, fortschrittlichste Technik, die nur bei FPS nützlich ist, ist die Verzögerungskompensation . Bei Verwendung der Verzögerungskompensation berücksichtigt der Server Clientverzögerungen, wenn er auf ein Ziel schießt. Wenn ein Spieler beispielsweise einen Kopfschuss auf seinem Bildschirm ausgeführt hat, sein Ziel jedoch in Wirklichkeit auf eine Verzögerung an anderer Stelle zurückzuführen ist, wäre es unehrlich, einem Spieler das Recht zu verweigern, aufgrund einer Verzögerung zu töten. Daher spult der Server die Zeit zurück zu dem Moment, in dem der Spieler geschossen hat, um zu simulieren, was der Spieler auf seinem Bildschirm gesehen hat, und um die Kollision zwischen seinem Schuss und dem Ziel zu überprüfen.

Glenn Fiedler (wie immer!) Schrieb 2004 einen Artikel in Network Physics (2004) , der den Grundstein für die Synchronisation von Physiksimulationen zwischen einem Server und einem Client legte. 2014 schrieb er eine neue Reihe von Artikeln zur Netzwerkphysik , in denen andere Techniken zur Synchronisation von Physiksimulationen beschrieben wurden.

Es gibt auch zwei Artikel im Valve-Wiki, Source Multiplayer Networking und Latency Compensating Methods im Client / Server In-Game-Protokolldesign und -optimierung , in denen die Verzögerungskompensation behandelt wird.

Betrugsprävention


Es gibt zwei Haupttechniken, um Betrug zu verhindern.

Erstens: Das Versenden von Schadpaketen durch Betrüger wird erschwert. Wie oben erwähnt, ist die Verschlüsselung ein guter Weg, um sie zu implementieren.

Zweitens: Ein autoritärer Server sollte nur Befehle / Eingaben / Aktionen empfangen. Der Client sollte den Status auf dem Server nur durch Senden von Eingaben ändern können. Jedes Mal, wenn die Eingabe empfangen wird, muss der Server sie vor dem Anwenden auf Gültigkeit überprüfen.

Anwendungslogik: Fazit


Ich empfehle Ihnen, eine Methode zur Simulation großer Verzögerungen und niedriger Aktualisierungsraten zu implementieren, um das Verhalten Ihres Spiels unter schlechten Bedingungen testen zu können, selbst wenn Client und Server auf demselben Computer ausgeführt werden. Dies wird die Implementierung von Verzögerungsglättungstechniken erheblich vereinfachen.

Andere nützliche Ressourcen


Wenn Sie andere Ressourcen in Netzwerkmodellen untersuchen möchten, finden Sie diese hier:

Source: https://habr.com/ru/post/de467025/


All Articles