Multiplayer-Entwicklung Grundlagen
Multiplayer verstehen - Teil I
Multiplayer ist ein komplexes Thema. Ich selbst habe mich lange dafür gedrückt, mich damit eingehend zu beschäftigen. Ein großes Problem ist, dass viele Tutorials, welche im Internet zu finden sind, zu schnell in's Detail gehen. Es wird sich wenig Zeit dafür genommen, das Thema in seinen Grundsätzen zu erklären. Mit einem theoretischen Grundwissen, was für Probleme und Fragestellungen mit dem Entwickeln eines Multiplayers einher gehen, machen alle Frameworks, Optionen und Werkzeuge weit aus mehr Sinn.
Deswegen werde ich in der folgenden Artikelreihe versuchen meine Erkenntnisse bei diesem Thema so verständlich wie möglich aufzuarbeiten – in der Hoffnung, dass es vielleicht anderen den Einstieg erleichtert.
Was ist Multiplayer?
Synchronisierung einer gemeinsamen Realität
Kurz gesagt, ist Multiplayer die Synchronisierung eines Game-States über ein Netzwerk mit mehreren Clients. Der Game-State enthält alle Informationen von zu synchronisierenden Objekten der Anwendung, wie z.B. Position, Rotation und Skalierung.
Bauen zwei Personen eine Verbindung zueinander auf und bewegt eine Person mithilfe eines Eingabegeräts ein Objekt in der virtuellen Welt und verändern damit seine Position, muss diese Änderung der Anwendung der zweiten Person mitgeteilt werden. Das bedeutet allerdings nicht, dass alles, was sich in der Anwendung verändert, synchronisiert werden muss.
Entwickler*innen sollten sich bewusst machen, ob ein Objekt in eines der folgenden Kategorien fällt:
- Kontinuierlich synchronisierte Objekte
- Eventbasiert synchronisierte Objekte
- Nicht synchronisierte Objekte
Es gilt, die Menge der Daten, welche synchronisiert werden müssen, so gering wie möglich zu halten. Hat ein Objekt, auch wenn es dynamisch ist, keinen Einfluss auf den Gameplay-Loop? Dann muss es auch nicht synchronisiert werden.
Nimmt man das Spiel EA Sports FC 24 als Beispiel. Die Akteure in dem Spiel wie beispielsweise der Torwart (A) fallen in die Kategorie "kontinuierlich synchronisierte Objekte". Alle Spieler:innen sollten neben der Position des Torwarts alle weiteren Daten erhalten, die für das Gameplay notwendig sind. Dabei haben diese Informationen eine hohe Priorität und sollten mit jedem Frame aktualisiert werden, damit alle Spieler:innen ihre Entscheidungen auf zuverlässigen Daten treffen können. Würden beispielsweise nur alle 200ms die Daten des Torwarts aktualisiert werden, wäre ein Spiel dieser Art unmöglich.
Der Punktestand (B) hingegen muss nicht mehrmals pro Sekunde über das Netzwerk aktualisiert werden. Tore fallen alle paar Minuten. Darum fällt die Synchronisierung dieses Objekts in die Kategorie "Eventbasiert synchronisierte Objekte". Wenn ein Tor fällt, wird der Punktestand aktualisiert und alle Clients werden benachrichtigt.
In dem Beispiel ist das Publikum auf den Tribünen (C) im Hintergrund animiert. Sie jubeln, Bewegen sich und tragen mit zu der Stimmung und der Immersion des Spiels bei. Allerdings ist für das Gameplay unerheblich, ob Fan #8572 im Hintergrund gerade seine Arme hochreißt, oder mit dem Schal wedelt. Solche Objekte fallen in die Kategorie "Nicht synchronisierte Objekte". Sie können sich von Client zu Client unterscheiden, ohne einen Einfluss zu haben.
Wie man sieht, ist es nicht notwendig, dass die gesamte Anwendung synchronisiert wird. Der Game-State ist nicht alles, was in einer Anwendung existiert, sondern nur das, was für das Gameplay notwendig ist.
Funktion folgt dem Design
Bei Simulationen und Spielen handelt es sich in der Regel um immersive, hoch interaktive Produkte, welche stetige Tests voraussetzen, um am Ende erfolgreich zu sein. Im Zuge der Entwicklung und der Tests entstehen neue Ideen, verändern sich Anforderungen, welche implementiert werden wollen, damit das bestmögliche Produkt entsteht. Es ist ein iterativer Prozess, der zum Ziel führt. Kurz gesagt: Das Gameplay verändert sich stetig und wird neu angepasst.
Wie wir aber im vorangegangenen Abschnitt gelernt haben, werden essenzielle Entscheidungen wie Multiplayer-Features implementiert werden müssen, vom Gameplay abhängig gemacht. Die Implementierung eines Multiplayer-Features setzt voraus, dass man sich festlegt, hat. Nicht nur das, die Funktionsweise der Anwendung entscheiden darüber hinaus, welche Infrastruktur-Lösung, Netzwerk-Topologie und Sicherheitsmaßnahmen notwendig sind. Spätere Umstiege sind zeitintensiv und damit unbedingt zu vermeiden.
Damit entsteht ein Konflikt zwischen der iterativen, meist agilen Natur von Projekten dieser Art und der Notwendigkeit, sich früh auf bestimmte Rahmenbedingungen festzulegen, um einen soliden Multiplayer zu entwickeln. Hierzu gibt eine keine einfache Lösung, sondern nur Mittelwege, für welche man sich entscheiden muss. Vorgefertigte Tooling-Landschaften, welche den gesamten Use-Case Multiplayer abdecken und dank einer erprobten API die Möglichkeit bieten, zwischen Lösungen zu wechseln, helfen enorm, doch können je nach Produkt kostenintensiv werden und werfen neue Fragen bezüglich Datenschutzes und Datensicherheit auf. Entwickler*innen müssen sich somit zunächst damit abfinden, dass die Integration eines Multiplayers den Entwicklungsprozess verlangsamen und einschränken wird.
Autorität über den State
Neben der Frage, welche Objekte synchronisiert werden müssen, muss sich jeweils die Frage gestellt werden, wer die Autorität über das Objekt besitzt und bestimmt, was die „Single Source of Truth“ ist – entweder ein Client oder der Server.
Standardmäßig verfolgen Multiplayer-SDKs den Ansatz Server-Authoritative. Bedeutet, dass nur der Server Änderungen an zu synchronisierenden Daten vornehmen kann. Werden diese Daten vom Server aktualisiert, werden diese an die verbundenen Clients versendet. Doch wenn alle synchronisierten Objekte vom Server verwaltet würden, gäbe es keinerlei Interaktion durch die Clients. Deswegen wird in den allermeisten Fällen der Input der Benutzer*innen kontinuierlich synchronisiert und als Client-Authoritative behandelt. Bedeutet, dass die Input-Daten, wie z.B. welche Tasten gedrückt sind, vom Client verändert werden können. Der Server, welcher die Autorität über die Position des zu steuernden Avatars besitzt, verarbeitet den Input, verändert dem entsprechend die Position und sendet diese neuen Daten an die Clients.
Server-Authoritative hat neben dem Vorteil, dass es Cheating verhindert, aber auch einen gravierenden Nachteil. Denn die Kommunikation zwischen Server und Clients besitzt in jedem Fall eine gewisse Latenz. Ein Client mit einer Verbindungs-Latenz von 500ms muss 0,5 Sekunden warten, bis der eigene Avatar auf einen Tastendruck reagiert. Je nach Anwendungsfall und Gameplay kann das zu Frust führen. Allgemein geben SDKs viele Werkzeuge zur Hand, um den State und die Autorität über den State zu managen. Beispielsweise kann das zuvor erwähnte Latenz-Problem mit Client-Side-Prediction oder Dead-Reckoning aufgelöst werden, was von einigen SDKs angeboten wird. Auf die verfügbare Tooling-Landschaft wird in einem späteren Abschnitt genauer eingegangen.
Missverständnisse
Bevor wir konkreter werden und auf Topologie und Software eingehen, welche für Multiplayer-Anwendungen notwendig sein können, ist es noch wichtig, Missverständnissen vorwegzunehmen. Bei den vielen Ressourcen, die sich zu dem Thema finden lassen, ist es nicht selten, dass sich ein falsches Verständnis festsetzt.
Simulated on the Server.
Häufig liest man diesen Ausdruck neben "Server sends back..." oder "Server manages data...". In den Köpfen kann dann das Bild entstehen, dass der Server eine Art Datenbank ist, welche über IDs die verschiedenen Datensätze verwaltet. Doch tatsächlich ist der Server nichts Weiteres als ein spezieller Build der gleichen Anwendung, die auch die Clients verwenden. Diese läuft ohne eine GUI – sozusagen headless – doch werden dort die gleichen Berechnungen vorgenommen, wie bei den Clients. Der große Unterschied ist, dass der Server standardmäßig über alle synchronisierten Objekte die Autorität besitzt und sozusagen die "Single Source of Truth" abbildet.
Server Builds.
Wie eben erwähnt, läuft auf einem Server ein besonderer Build der Anwendung. Dabei ist es wichtig zu verstehen, dass es sich dabei um einen Build desselben Source-Codes handelt, welchen auch die Clients verwenden. Eine Trennung von Frontend und Backend, wie man es häufig aus der klassischen Software-Entwicklung kennt, gibt es nicht. Server und Client Code existieren parallel, oftmals teilen sie sich dieselben Klassen. Was zunächst für Verwirrung sorgen kann, ist eine Notwendigkeit, da sonst bestimmte Werkzeuge zur Synchronisierung der Game-States, wie z.B. Remote-Procedure Calls auf die wir später eingehen werden, nicht möglich wären.
Ein erster Überblick
In diesem ersten Teil der Multiuser-Reihe haben wir uns mit dem grundsätzlichen Verständnis einer solchen Anwendung beschäftigt und wie das Multiuser-Feature von bestehenden Features beeinflusst wird. Auch ging es darum, welche Herausforderungen gezwungenermaßen mit einem Multiuser-Feature einhergehen. Im nächsten Beitrag gehen wir weiter ins Detail und werden uns der Infrastruktur- und Software-Landschaft rund um das Thema Multiuser genauer ansehen.