Unity: Start in die Multiplayerprogrammierung mit Netcode for Game Objects

Inhalt:

1. Was bietet Netcode for Game Objects?

Unity Netcode ermöglicht die Implementierung von Multiplayer-Spielen, indem es Funktionen und Werkzeuge für die Netzwerkkommunikation bereitstellt. Das Prinzip besteht darin, dass Spieler über das Netzwerk miteinander kommunizieren können, um Daten auszutauschen, wie z.B. Spielerpositionen, Aktionen oder Spielzustände.

Unity Netcode bietet Funktionen wie Server-Client-Architektur, Synchronisation von Spielobjekten, Autoritätssysteme und vieles mehr. Entwickler können Netzwerkcode schreiben, um sicherzustellen, dass die Spielwelt auf allen Clients konsistent bleibt.

Cover des Artikels über Unity Netcode

2. Wichtige theoretische Grundlagen

Natürlich könnten wir jetzt gleich munter drauf los programmieren und testen, was Unity Netcode kann. Du solltest dich trotzdem zuvor mit einigen wichtigen Grundlagen vertraut machen.

2.1 Was versteht man unter Server-Client-Architektur?

Die Server-Client-Architektur ist ein grundlegendes Konzept in der Informatik, das auch in der Welt der Multiplayer-Spiele weit verbreitet ist. Es bezieht sich auf die Aufteilung der Verantwortlichkeiten und Funktionen zwischen zwei Arten von Systemen: dem Server und den Clients. Diese Architektur wird verwendet, um Multiplayer-Spiele zu organisieren und die Kommunikation zwischen den Teilnehmern zu koordinieren.

Unity Netcode bietet Funktionen wie Server-Client-Architektur, Synchronisation von Spielobjekten, Autoritätssysteme und vieles mehr. Entwickler können Netzwerkcode schreiben, um sicherzustellen, dass die Spielwelt auf allen Clients konsistent bleibt.

    Aufgaben des Servers:

  • Verwaltung des Spielzustands
  • Ausübung der Autorität
  • Authentifizierung der einzelnen Benutzer
  • Kommunikation zwischen den Clients verwalten
  • Synchronisation von Spieldaten


    Clients sind die Endgeräte oder Computer, auf denen die Spieler das Spiel erleben:

  • Anzeige der Spielgrafik
  • Annehmen von Benutzereingaben
  • Umsetzung lokaler Simulationen

Zwischen dem Server findet ständig eine Kommunikation statt. Die Clients übermitteln dabei ihren aktuellen Zustand und erhalten vom Server angeordnete Änderungen.


Wir wollen uns nachfolgend zwei weit verbreitete Architekturmodelle ansehen.

2.2 Vom Client gehostet

Überblick Host-Architektur

Bei der Host-Architektur fungiert der Spieler, der das Spiel startet, sowohl als Client als auch als Host. Dies bedeutet, dass dieser Spieler die Rolle des Servers übernimmt und gleichzeitig als lokaler Client am Spiel teilnimmt. Andere Spieler können sich dann mit diesem Host verbinden, um am Spiel teilzunehmen.



Dabei treten Vorteile aber auch Nachteile auf:

    Vorteile:

  • Kostenlos: Keine Kosten im Vergleich zu dedizierten Servern
  • Einfach: Die Host-Architektur erfordert keine besondere Infrastruktur


    Nachteile:

  • Leistung: Abhängig von der häuslichen Internetverbindung des Host-Players und der Leistungsfähigkeit seines Computers
  • Latenz: Der Gastgeberspieler kann einen großen Latenzvorteil gegenüber den anderen Spielern haben
  • Betrug: Mit Zugang zu allen Informationen der Spielwelt hat der Gastgeberspieler eine einfachere Gelegenheit zu betrügen
  • Abbruch: Das Spiel endet automatisch, wenn der Host-Spieler das Spiel verlässt.

2.3 Dedizierter Game Server

Überblick Server-Architektur

Die dedizierte Serverarchitektur ist eine Multiplayer-Spielarchitektur, bei der ein spezieller Server die zentrale Rolle bei der Verwaltung des Spiels übernimmt.

Im Gegensatz zur Client-Host-Architektur wird bei einer dedizierten Serverarchitektur ein unabhängiger Server verwendet.

Die dedizierten Server simulieren Spielwelten, ohne die direkte Eingabe oder Ausgabe zu unterstützen, außer die, die für ihre Verwaltung erforderlich sind. Spieler müssen sich mit separaten Client-Programmen mit dem Server verbinden, um das Spiel zu sehen und zu interagieren.

Diese Architektur wird häufig in größeren Multiplayer-Spielen eingesetzt, d.h. es kann eine große Anzahl von Spielern verwaltet werden.

Auch das hat Vor- und Nachteile:

    Vorteile:

  • Skalierbarkeit: Dedizierte Server können eine größere Anzahl an Spielern verwalten
  • Fairness: Da der Server die endgültige Autorität über den Spielzustand hat, ist Betrug schwieriger
  • Zuverlässig: Dedizierte Server sind auf die Anforderungen von Multiplayer-Spielen abgestimmt und bieten eine zuverlässige und konsistente Spielerfahrung


    Nachteile:

  • Komplex: Die Implementierung und Wartung ist aufwendiger als bei der Host-Architektur
  • Kosten: Der Betrieb des Servers ist mit Kosten verbunden
  • Zu groß: Bei Spielen mit wenigen Spielern überdimensioniert und unötig teuer
  • Leistung: Spielerfahrung ist stark abhängig von der Leistung, Verfügbarkeit und reibungslosen Funktion des Servers

2.4 Latenz

Latenz bezieht sich auf die Verzögerung oder die Zeitspanne zwischen dem Senden eines Datenpakets von einem Absender zum Empfänger und dem Empfangen des Pakets durch den Empfänger.

Im Kontext von Spielen bedeutet das den Zeitunterschied zwischen Ursache und sichtbarer Wirkung. Beispielsweise eine Verzögerung zwischen der Betätigung einer Taste und der Bewegung der Spielfigur.

    Hohe Latenz kann zu folgenden Problemen führen:

  • Schlechte Spielerfahrung durch verzögerte Reaktionen
  • Falls Latenzen durch Interpolation überbrückt werden, kann das zu Ungenauigkeiten und Unterschieden zwischen den verschiedenen Spielwelten führen
  • Oft geht hohe Latenz mit Datenverlust einher, was die Spielererfahrung weiter verschlechtert

Diese Zeit zwischen dem Senden der Anfrage und dem Empfang der Antwort vom Server wird auch als Ping bezeichnet. Ein Ping von 20ms bedeutet, das die Übertragung der Anfrage vom Client zum Server und die Antwort zurück zum Client 20ms benötigt.

Grafik Erklärung Ping

2.5 Datenverlust

Datenverlust tritt auf, wenn Datenpakete während der Übertragung zwischen Absender und Empfänger verloren gehen und nicht am Ziel ankommen.

    Datenverlust kann zu folgenden Problemen führen:

  • Inkonsistene Spielwelt, das bedeutet Clients werden über Änderungen der Spielwelt nicht informiert
  • Verzögerte und schlechte Aktualisierung der Spielwelt
  • Unvorhersehbare Spieleraktionen durch verzögerte oder falsche Aktualisierung

Durch lokale Vorhersage von Aktionen auf dem Client und anschließendem Abgleich mit den Daten vom Server, können Datenverluste und auch Latenz teilweise ausgeglichen werden. Auch durch die Implementierung von Mechanismen zur Erkennung und Korrektur von Datenverlusten kann die Integrität der Spielzustands bewahrt werden.

2.6 Die Transportschicht

Die Transportschicht sorgt für zuverlässigen und ordnungsgemäßen Transport von Daten zwischen den Endgeräten innerhalb eines Netzwerks. Sie kümmert sich um die Verbindungsherstellung, den Datenfluss und die Fehlerbehandlung während der Kommunikation.

    Wichtige Aspekte der Transportschicht:

  • Zuverlässige und fehlerfreie Kommunikation wird am besten durch das Transmission Control Protocol (TCP) gewährleistet. TCP stellt sicher, dass Datenpakete in der richtigen Reihenfolge ankommen, überprüft auf Fehler, fordert fehlende Pakete erneut an und bietet Mechanismen zur Flusskontrolle und Überlastkontrolle.
  • Das User Datagram Protocol (UDP) ist ein einfacheres Protokoll auf der Transportschicht im Vergleich zu TCP. UDP ist geeignet für Anwendungen, bei denen geringe Latenz wichtiger ist als die Gewährleistung der vollständigen Übertragung von Daten, wie zum Beispiel bei Echtzeitübertragungen von Audio oder Video. Es bietet unzuverlässige, nicht bestätigte Übertragung von Daten.
  • Auf der Transportschicht werden Portnummern verwendet, um verschiedene Dienste oder Anwendungen auf einem Endgerät zu identifizieren. Portnummern ermöglichen es, dass mehrere Anwendungen gleichzeitig auf einem Gerät laufen können.
  • Die Transportschicht ist für die Segmentierung großer Datenmengen in kleinere Einheiten verantwortlich, die dann über das Netzwerk übertragen werden können. Auf der Empfangsseite werden die Segmente wieder zusammengefügt, um die ursprünglichen Daten wiederherzustellen.
  • Die Transportschicht ermöglicht die Ende-zu-Ende-Kommunikation zwischen Anwendungen auf verschiedenen Endgeräten im Netzwerk. Sie sorgt dafür, dass Daten von der Quellanwendung zur Zielanwendung übertragen werden können.

Unity Transport Layer (UTP) ist eine Abstraktionsschicht in der Unity Game Engine, die den Entwicklern Funktionen und Werkzeuge zur Netzwerkkommunikation für Multiplayer-Spiele bereitstellt. Dabei handelt es sich um eine von Unity bereitgestellte Netzwerkschicht, die verschiedene Transportprotokolle wie TCP oder UDP verwenden kann.

2.7 Das Autoritätssystem

Ein Autoritätssystem hat die Kontrolle und Entscheidungsbefugnis darüber, welcher Teil des Spiels von welcher Instanz (Server oder Client) gesteuert wird. Das Autoritätssystem ist entscheidend, um die Integrität des Spiels sicherzustellen, faire Bedingungen für alle Spieler zu gewährleisten und potenzielle Cheating-Versuche zu minimieren.

Ein gutes Autoritätssystem hilft, das Spiel fair und konsistent zu halten, indem es verhindert, dass Clients unbeabsichtigte oder betrügerische Aktionen durchführen können, die den Spielspaß für andere beeinträchtigen könnten.

    Wichtige Aspekte eines Autoritätssystems:

  • Server-Autorität:
    Der Game-Server hat normalerweise die höchste Autorität im Spiel. Das bedeutet, dass der Server die endgültige Kontrolle über wichtige Aspekte des Spiels hat, wie z. B. die Ausführung der Spielregeln und die Bestimmung des globalen Spielzustands. Kritische Operationen, die das Spiel beeinflussen könnten, werden vom Server kontrolliert, um sicherzustellen, dass alle Spieler die gleichen Informationen und den gleichen Spielzustand haben.
  • Client-Autorität:
    In einigen Fällen kann einem Client vorübergehend die Autorität über bestimmte Spielobjekte oder Aktionen erteilt werden. Dies ermöglicht eine reaktionsschnelle Benutzererfahrung, da der Client lokal Aktionen vorhersagen und anzeigen kann, ohne auf eine Bestätigung vom Server warten zu müssen. Der Server muss die vom Client initiierten Aktionen jedoch zuvor immer prüfen, um sicherzustellen, dass sie den regeln entsprechen.
  • Autoritätstransfer:
    In einigen Szenarien kann die Autorität über ein Objekt während des Spiels zwischen Server und Clients wechseln. Dies ist wichtig, um eine reibungslose und konsistente Spielerfahrung zu gewährleisten, insbesondere wenn sich die Zuständigkeiten ändern.

2.8 Relaisserver

Netcode ermöglicht es, sich mit einem Host mit seiner IP und seinem Port zu verbinden. Wenn der Host jedoch nicht im selben Netzwerk wie die Clients ist (zum Beispiel irgendwo im Internet), kann ein Relaisserver helfen.

Ein Relaisserver ist ein Server, der als Vermittler zwischen zwei oder mehr Endgeräten (Clients) fungiert, um Kommunikation herzustellen, wenn direkte Verbindungen zwischen den Clients aufgrund von Netzwerkbeschränkungen oder Firewall-Einstellungen nicht möglich sind.

Er übernimmt die Aufgabe, Daten zwischen den Clients weiterzuleiten, um eine indirekte Kommunikation zu ermöglichen.

Der Server ist im Internet mit einer öffentlichen IP, die die Clients und der Host erreichen können. Nachdem jede Seite mit dem Relais verbunden ist, können sie eine Verbindung herstellen und über den Relay-Server Daten senden.

Unity Services bietet einen Relaisservice, der alle auf Unity Transport basierenden Technologien wie Netcode weiterleiten kann.

Werbung (Affiliate - ich bekomme eine kleine Provision, dein Preis ändert sich nicht)

3. Einfaches Beispiel: Spieler spawnen und bewegen

Bevor wir loslegen können, müssen wir uns noch ein paar einfache Komponenten ansehen, die mindestens nötig sind, um Multiplayer Spiele mit Unity Netcode herzustellen.

Zunächst wollen wir ein PlayerPrefab erstellen. Dieses stellt unseren Spieler dar, der später vom NetworkManager ins Spiel eingefügt wird.

3.1 NetworkObject

Jedes Objekt, welches im Netzwerk synchronisiert werden soll, benötigt zwingend eine NetworkObject-Komponente. Auch Remote-Procedure-Calls(RPC) können nur von Gameobjekten mit NetworkObject-Komponente durchgeführt werden.

Weiterhin funktionieren andere Netzwerkkomponenten wie NetworkTransform oder Netzwerk-Variablen und die Klasse NetworkBehaviour nur mit einer NetworkObject-Komponente auf dem gleichen GameObject.

Das NetworkObject sollte sich immer im Parentobjekt eines Prefabs befinden und nicht eigeschachtelt sein.

Wenn ein NetworkObject übergeordnet wird, synchronisiert Netcode sowohl die übergeordneten Informationen als auch die Transformationswerte des untergeordneten Objekts. Um zu entscheiden, ob lokalen oder weltbezogene Transformationswerte der untergeordneten NetworkObject-Komponente synchronisiert werden sollen, wird die Eigenschaft WorldPositionStays verwendet.

Screenshot einer NetworkObject-Komponente

3.2 NetworkTransform

Diese Komponente wird zur Synchronisation der Transformation eines Netzwerkobjektes verwendet. Mit anderen Worten, es ist notwendig um die Position, Drehung und Skalierung in der Spielwelt zu synchronisieren.

NetworkTransform benötigt zwingend eine NetworkObject-Komponente um zu funktionieren.

NetworkTransform-Komponenten dürfen verschachtelt werden.

Screenshot einer NetworkTransform-Komponente

3.3 NetworkBehaviour

NetworkBehaviour ist eine abgeleitete Klasse von MonoBehaviour in Unity. NetworkBehaviour ist die Basisklasse für Skripte, die Netzwerkfunktionalität in Unity implementieren sollen. Wir nutzen es natürlich auch für die Spielfigur. Mit Hilfe von NetworkBehaviour, lässt sich bestimmen, welche Teile des Codes lokal und welche über das Netzwerk ausgeführt werden sollen.

Durch NetworkBehaviour lassen sich u.a. synchronisierte Variablen verwenden und Remote-Procedure-Calls(RPC) verwenden.

Weiterhin bietet die Klasse Methoden um wichtige Netzwerkereignisse zu managen.


    Wichtige Properties von NetworkBehaviour:
  • public bool IsClient {get;}: Wenn als Client ausgeführt
  • public bool IsHost {get;}: Wenn als Host ausgeführt
  • public bool IsLocalPlayer {get;}: Wenn als lokaler Spieler ausgeführt
  • public bool IsOwner {get;}: Wenn das Objekt dem lokalen Spieler gehört
  • public bool IsServer {get;}: Wenn als Server ausgeführt

    Wichtige Methoden von NetworkBehaviour:
  • public virtual void OnNetworkSpawn(): Wenn das NetworkObject erzeugt wird und das Netzwerk fertig eingerichtet ist
  • public virtual void OnNetworkDespawn(): Wird aufgerufen, wenn das NetworkObject despawnt
  • public virtual void OnDestroy(): Bei Zerstörung des NetworkObjects
  • public virtual void OnGainedOwnership(): Wenn der lokale Client Owner wird
  • public virtual void OnLostOwnership(): Wenn die Eigentümerschaft verloren geht

Weiter Methoden und Properties in der Unity Dokumentation

Nachfolgend erstellen wir mit NetworkBehaviour einen sehr einfache Spielercontroller:

3.4 PlayerPrefab erstellen

Die obigen 3 Komponenten bilden nun zusammen unsere Spielfigur. Im Beispiel wurden sie einer Kapsel hinzugefügt.

Aus dem Spieler-GameObject wird anschließend ein Prefab erstellt, dazu wird es in den zuvor erzeugten Prefab-Ordner im Projekt gezogen.

Animation die zeigt wie das PlayerPrefab erzeugt wird

3.5 NetworkManager und TransportLayer

Der NetworkManager ist für die Verbindung, Synchronisation und Kommunikation zwischen den verschiedenen Spielinstanzen (Clients und Server) zuständig. Ausserdem spawned er die Spieler und Netzwerk-Prefabs.

Unser Networkmanager-GameObject wird aus der Unity-Komponente NetworkManager und der bereits im Abschnitt 2 erwähnten Komponente: UnityTransport(UTP) bestehen.

Screenshot von NetworkManager und Transport Layer

Anschließend weisen wir dem NetworkManager unser PlayerPrefab zu.

Verbinden der PlayerPrefabs

3.6 Multiplayerfunktionen testen

Um unser Projekt zu testen könnten wir nun unserer Projekt kompilieren und diese zweite Instanz zusammen mit der Instanz im Editor testen. Da dies eher umständlich ist nutzen wir hier ParrelSync.

ParrelSync erstellt einen Clone des Projekts, sodass wir in einem zweiten Editor die zweite Instanz testen können. Änderungen im Projekt werden immer nur im Hauptprojekt getätigt und automatisch vom Clone übernommen.

Um das Projekt zuverwenden, füge "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync" im PackageManger unter dem Punkt "Add Package from git url" ein.

Alternativ kannst Du das Projekt auch runterladen und den Ordner "ParrelSync" in das Unity Project Fenster ziehen.


ParrelSync Editor um einen Clone zu erzeugen

Anschließend kannst Du einen Clone erzeugen und diesen in einem neuen Editor öffnen.


ParrelSync Clone und Editor

Jetzt kannst Du beide Editoren starten und jeweils mit der "Start Host" oder "Start Client" Taste im NetworkManager den beiden Instanzen des Spiels beitreten.

Host und Client starten

Wenn Du unser Projekt testest, wird dir auffallen, dass der zweite Spieler sich nicht bewegen kann. Das liegt daran, das der Server die Autorität über die Transformation des Spielers hat.

Client bewegt sich nicht

Wir können aber durch eine kleine Änderung dem Client die Autorität über seine Bewegung geben. Dazu erbt die folgende Klasse "ClientNetworkTransform" von NetworkTransform und überschreibt die Methode "OnIsServerAuthoritative()", so dass diese false zurückgibt.


Anschließend tauschen wir im Player-Prefab die NetworkTransform-Komponente gegen die neue abgeleitete Klasse. Sinnvoll ist ausserdem, nur die Position zu synchronisieren, denn Scale und Rotation werden hier nicht benötigt.

Screenshot ClientNetworkTransform

Nun können sich beide Spieler bewegen und die Position wird synchronisiert.

Client bewegt sich jetzt

4. Grundlagen zur Synchronisation über Transform hinaus

Natürlich ist die Synchronisation der Transformation von Spielobjekten nicht alles. Beispielsweise müssen auch Lebenspunkte oder Texte oder auch Animationen synchronisiert werden.

Unity Netcode bietet dazu verschiedene Möglichkeiten wie ClientRpc, ServerRpc oder synchronisierte Variablen. Mehr zur Synchronisation auch in der Unity Dokumentation

4.1 ServerRpc

ServerRpc bieten die Möglichkeit, Informationen von einem Client an den Server zu senden. Ein ServerRpc ist ein Remote Procedure Call (RPC), der nur von einem Client aufgerufen werden kann und immer auf dem Server bzw.Host empfangen und ausgeführt wird.

Ein ServerRpc wird durch ein vorstehendes Attribut [ServerRpc] deklariert, zusätzlich bekommt der Methodenname das Suffix "ServerRpc".

4.2 ClientRpc

Server können einen ClientRpc aufrufen, um ihn auf allen Clients auszuführen. ClientRpc werden deklariert, indem das Attribut [ClientRpc] vor die Methode geschrieben wird und das Suffix "ClientRpc" in den Methodennamen eingefügt wird.


4.3 NetzwerkVariablen

NetworkVariable ist eine Möglichkeit, Werte zwischen einem Server und den Clients zu synchronisieren, ohne RPCs verwenden zu müssen.


Mehr zu Netzwerkvariablen in der Unity Dokumentation

5. Schlusswort

In diesem Grundkurs wollte ich dir die Grundlagen von Unity Netcode for Gameobjects vorstellen. Ich würde mich freuen, wenn dir dieses Tutorial den Start erleichtert hat und dich zum Weitermachen animiert.

Du findest alle weiteren Informationen auch in der Dokumentation von Unity Netcode.