Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

High-Level-Multiplayer

High-Level vs. Low-Level-API

Im Folgenden werden die Unterschiede zwischen High-Level- und Low-Level-Netzwerkfunktionen in Godot sowie einige grundlegende Aspekte erläutert. Wenn Sie sofort loslegen und Ihre ersten Nodes mit Netzwerkfunktionen ausstatten wollen, lesen Sie bitte weiter unten unter Netzwerkinitialisierung. Aber lesen Sie später unbedingt den Rest!

Godot unterstützte immer Standard-Low-Level-Netzwerkfunktionen über UDP, TCP und einige High-Level-Protokolle, wie HTTP und SSL. Diese Protokolle sind flexibel und können für fast alles verwendet werden. Allerdings kann die manuelle Synchronisierung des Spielstatus eine Menge Arbeit bedeuten. Manchmal lässt sich dieser Aufwand nicht vermeiden oder ist es wert, zum Beispiel wenn man mit einer eigenen Server-Implementierung im Backend arbeitet. In den meisten Fällen lohnt es sich jedoch, Godots High-Level Netzwerk-API zu verwenden, die einige der feineren Steuerungsmöglichkeiten der Low-Level Netzwerkfunktionen zugunsten größerer Benutzerfreundlichkeit opfert.

Dies ist auf die inhärenten Beschränkungen der Low-Level-Protokolle zurückzuführen:

  • TCP stellt sicher, dass Pakete immer zuverlässig und in der richtigen Reihenfolge ankommen, allerdings ist die Latenz durch Fehlerkorrektur in der Regel höher. Es ist auch ein etwas komplexeres Protokoll, da es versteht, was eine "Verbindung" ist, und für Ziele optimiert wird, die häufig nicht für Anwendungen wie Multiplayer-Spiele geeignet sind. Pakete werden gepuffert, um in größeren Batches gesendet zu werden, wobei weniger Overhead pro Paket gegen eine höhere Latenz getauscht wird. Dies kann für Anwendungen wie HTTP nützlich sein, aber im Allgemeinen nicht für Spiele. Einige davon können konfiguriert und deaktiviert werden (z.B. durch Deaktivieren des "Nagle-Algorithmus" für die TCP-Verbindung).

  • UDP ist ein einfacheres Protokoll, das nur Pakete sendet (und kein Konzept einer "Verbindung" hat). Die fehlende Fehlerkorrektur macht es ziemlich schnell (geringe Latenz), aber Pakete können auf dem Weg verloren gehen oder in der falschen Reihenfolge empfangen werden. Hinzu kommt, dass die MTU (maximale Paketgröße) für UDP im Allgemeinen niedrig ist (nur einige hundert Bytes). Wenn Sie also größere Pakete übertragen, müssen Sie diese aufteilen, neu organisieren und erneut versuchen, falls ein Teil fehlschlägt.

Im Allgemeinen kann TCP als zuverlässig, geordnet und langsam und UDP als unzuverlässig, ungeordnet und schnell charakterisiert werden. Wegen des großen Performance-Unterschieds macht es häufig Sinn, die Teile von TCP wiederzuverwenden, die für Spiele gewollt sind (optionale Zuverlässigkeit und Paketreihenfolge) , während die ungewollten Teile (Paketstau/Übertragungs-Kontroll-Features, Nagle-Algorithmus, etc.) weggelassen werden. Aus diesem Grund liefern die meisten Spiele-Engines solche Implementierungen direkt mit und Godot ist da keine Ausnahme.

Zusammengefasst kann die Low-Level Netzwerk-API für maximale Kontrolle genutzt werden, wobei alles darüber von Grund auf selbst implementiert werden muss, oder man nutzt die High-Level API basierend auf dem Szenenbaum, die den Großteil der Arbeit im Hintergrund leistet und das im Allgemeinen auf optimierte Weise.

Bemerkung

Die meisten von Godot unterstützten Plattformen liefern alle oder die meisten der angesprochenen High- und Low-Level Netzwerk-Features. Da Netzwerkfunktionen allerdings sehr plattformabhängig sind, könnten manche Features auf manchen Plattformen nicht verfügbar sein. Besonders die HTML5-Plattform unterstützt zurzeit nur WebSocket und manche High-Level-Features, als auch der Zugriff auf Low-Level-Protokolle wie TCP und UDP, fehlen.

Bemerkung

Mehr über TCP/IP, UDP und Netzwerkfunktionen im Allgemeinen finden Sie hier: https://gafferongames.com/post/udp_vs_tcp/

Gaffer On Games hat viele nützliche Artikel über Netzwerkfunktionen in Spielen (hier), darunter die umfassende Introduction to networking models in games.

Wenn Sie die Low-Level-Netzwerkbibliothek Ihrer Wahl anstatt der in Godot eingebauten nutzen wollen, finden Sie hier ein Beispiel: https://github.com/PerduGames/gdnet3

Warnung

Das Hinzufügen von Netzwerkfunktionen zu Ihrem Spiel ist mit einer gewissen Verantwortung verbunden. Es kann Ihre Anwendung angreifbar machen, wenn es falsch gemacht wird, und kann zu Cheats oder Exploits führen. Es kann sogar einem Angreifer ermöglichen, die Computer zu kompromittieren, auf denen Ihre Anwendung ausgeführt wird, und Ihre Server zu verwenden, um Spam zu senden, andere anzugreifen oder die Daten Ihrer Benutzer zu stehlen, wenn diese Ihr Spiel spielen.

Das ist bei Netzwerkanwendungen immer der Fall, ganz unabhängig ob Godot genutzt wird oder nicht. Sie können natürlich experimentieren, aber wenn Sie eine Netzwerkanwendung veröffentlichen, berücksichtigen Sie immer mögliche Sicherheitsbedenken.

Mid-Level-Abstraktion

Bevor wir darauf eingehen, wie wir ein Spiel über das Netzwerk synchronisieren möchten, kann es hilfreich sein zu verstehen, wie die grundlegende Netzwerk-API für die Synchronisierung funktioniert.

Godot verwendet ein Mid-Level-Objekt MultiplayerPeer. Dieses Objekt soll nicht direkt erstellt werden, sondern ist so konzipiert, dass mehrere C++-Implementierungen es bereitstellen können.

Dieses Objekt ist eine Erweiterung von PacketPeer, so dass es alle nützlichen Methoden zum Serialisieren, Senden und Empfangen von Daten erbt. Darüber hinaus fügt es Methoden hinzu, um einen Peer, den Übertragungsmodus usw. festzulegen. Sie enthält auch Signale, die Ihnen mitteilen, wenn Peers sich verbinden oder die Verbindung trennen.

Diese Klassenschnittstelle kann die meisten Arten von Netzwerkschichten, Topologien und Bibliotheken abstrahieren. Standardmäßig bietet Godot eine Implementierung auf Basis von ENet (ENetMultiplayerPeer), eine auf Basis von WebRTC (WebRTCMultiplayerPeer) und eine auf Basis von WebSocket (WebSocketPeer), aber dies könnte verwendet werden, um mobile APIs (für Ad-hoc-WiFi, Bluetooth) oder benutzerdefinierte geräte-/konsolenspezifische Netzwerk-APIs zu implementieren.

In den meisten Fällen wird davon abgeraten, dieses Objekt direkt zu verwenden, da Godot noch leistungsfähigere Netzwerkfunktionen bietet. Dieses Objekt wird dennoch zur Verfügung gestellt, falls ein Spiel spezielle Anforderungen an eine Low-Level-API hat.

Überlegungen zum Hosting

Wenn Sie einen Server hosten, können sich Clients in Ihrem LAN über die interne IP-Adresse verbinden, die normalerweise die Form 192.168.*.* hat. Diese interne IP-Adresse ist nicht für Nicht-LAN-/Internet-Clients erreichbar.

Unter Windows können Sie Ihre interne IP-Adresse herausfinden, indem Sie eine Eingabeaufforderung öffnen und ipconfig eingeben. Unter macOS öffnen Sie ein Terminal und geben ifconfig ein. Unter Linux öffnen Sie ein Terminal und geben ip addr ein.

Wenn Sie einen Server auf Ihrem eigenen Rechner hosten und möchten, dass Nicht-LAN-Kunden eine Verbindung herstellen können, müssen Sie wahrscheinlich den Server-Port auf Ihrem Router per Forwarding weiterleiten. Dies ist erforderlich, damit Ihr Server vom Internet aus erreichbar ist, da die meisten privaten Verbindungen ein NAT verwenden. Godots High-Level-Multiplayer-API verwendet nur UDP, also müssen Sie den Port in UDP weiterleiten, nicht nur in TCP.

Nachdem Sie einen UDP-Port weitergeleitet und sichergestellt haben, dass Ihr Server diesen Port verwendet, können Sie mit Hilfe dieser Website Ihre öffentliche IP-Adresse ermitteln. Geben Sie dann diese öffentliche IP-Adresse an alle Internet-Clients weiter, die eine Verbindung zu Ihrem Server herstellen möchten.

Godots High-Level-Multiplayer-API verwendet eine modifizierte Version von ENet, die eine vollständige IPv6-Unterstützung ermöglicht.

Initialisieren des Netzwerks

High-Level-Netzwerkfunktionen in Godot werden durch den SceneTree verwaltet.

Jeder Node hat eine multiplayer-Property, die eine Referenz auf die MultiplayerAPI-Instanz ist, die für ihn durch den Szenenbaum konfiguriert wurde. Anfänglich ist jeder Node mit dem gleichen Standard MultiplayerAPI Objekt konfiguriert.

Es ist möglich, ein neues MultiplayerAPI-Objekt zu erzeugen und es einem NodePath im Szenenbaum zuzuweisen, wodurch multiplayer für den Node an diesem Pfad und alle seine Unter-Nodes außer Kraft gesetzt wird. Dies erlaubt es, Nachbar-Nodes mit verschiedenen Peers zu konfigurieren, was es möglich macht, einen Server und einen Client gleichzeitig in einer Instanz von Godot laufen zu lassen.

# By default, these expressions are interchangeable.
multiplayer # Get the MultiplayerAPI object configured for this node.
get_tree().get_multiplayer() # Get the default MultiplayerAPI object.

Um die Netzwerkfunktionen zu initialisieren, muß ein MultiplayerPeer-Objekt erzeugt, als Server oder Client initialisiert und an die MultiplayerAPI übergeben werden.

# Create client.
var peer = ENetMultiplayerPeer.new()
peer.create_client(IP_ADDRESS, PORT)
multiplayer.multiplayer_peer = peer

# Create server.
var peer = ENetMultiplayerPeer.new()
peer.create_server(PORT, MAX_CLIENTS)
multiplayer.multiplayer_peer = peer

Beenden von Netzwerkfunktionen:

multiplayer.multiplayer_peer = null

Warnung

Wenn Sie nach Android exportieren, stellen Sie sicher, dass Sie die Berechtigung INTERNET in der Android-Exportvorgabe aktivieren, bevor Sie das Projekt exportieren oder die Ein-Klick-Auslieferung verwenden. Andernfalls wird jede Art von Netzwerkkommunikation von Android blockiert.

Verbindungen verwalten

Jedem Peer wird eine eindeutige ID zugewiesen. Die ID des Servers ist immer 1, und den Clients wird eine zufällige positive Integer-Zahl zugewiesen.

Das Reagieren auf Verbindungen oder Verbindungsabbrüche ist durch die Verbindung mit den Signalen der MultiplayerAPI möglich:

  • peer_connected(id: int) Dieses Signal wird mit der ID des neu verbundenen Peers an jeden anderen Peer gesendet, und an den neuen Peer mehrfach, einmal mit der ID jedes anderen Peers.

  • peer_disconnected(id: int) Dieses Signal wird bei jedem verbleibenden Peer ausgegeben, wenn dieser die Verbindung trennt.

Der Rest wird nur auf Clients ausgesendet:

  • connected_to_server()

  • connection_failed()

  • server_disconnected()

Um die eindeutige ID des zugehörigen Peers zu erhalten:

multiplayer.get_unique_id()

Um zu prüfen, ob der Peer ein Server oder ein Client ist:

multiplayer.is_server()

Remote-Prozeduraufrufe

Remote Procedure Calls (RPCs) sind Funktionen, die auf anderen Peers aufgerufen werden können. Um einen RPC zu erstellen, verwenden Sie die @rpc-Annotation vor einer Funktionsdefinition. Um einen RPC aufzurufen, verwenden Sie die Methode rpc() von Callable, um jeden Peer aufzurufen, oder rpc_id(), um einen bestimmten Peer aufzurufen.

func _ready():
    if multiplayer.is_server():
        print_once_per_client.rpc()

@rpc
func print_once_per_client():
    print("I will be printed to the console once per each connected client.")

RPCs serialisieren keine Objekte oder Callables.

Damit ein Remote Call erfolgreich ist, müssen der sendende und der empfangende Node den gleichen NodePath haben, was bedeutet, dass sie den gleichen Namen haben müssen. Wenn Sie add_child() für Nodes verwenden, von denen erwartet wird, dass sie RPCs benutzen, setzen Sie das Argument force_readable_name auf true.

Warnung

If a function is annotated with @rpc on the client script (resp. server script), then this function must also be declared on the server script (resp. client script). Both RPCs must have the same signature which is evaluated with a checksum of all RPCs. All RPCs in a script are checked at once, and all RPCs must be declared on both the client scripts and the server scripts, even functions that are currently not in use.

The signature of the RPC includes the @rpc() declaration, the function, return type, AND the nodepath. If an RPC resides in a script attached to /root/Main/Node1, then it must reside in precisely the same path and node on both the client script and the server script. Function arguments (example: func sendstuff(): and func sendstuff(arg1, arg2): will pass signature matching).

If these conditions are not met (if all RPCs do not pass signature matching), the script may print an error or cause unwanted behavior. The error message may be unrelated to the RPC function you are currently building and testing.

See further explanation and troubleshooting on this post.

Die Annotation kann eine Reihe von Argumenten annehmen, die Default-Werte haben. @rpc ist äquivalent zu:

@rpc("authority", "call_remote", "unreliable", 0)

Die Parameter und ihre Funktionen sind wie folgt:

mode:

  • "authority": Nur die Multiplayer-Autorität (der Server) kann aus der Remote-Calls ausführen.

  • "any_peer": Clients dürfen Remote-Calls ausführen. Nützlich für die Übermittlung von Benutzereingaben.

sync:

  • "call_remote": Die Funktion wird nicht auf dem lokalen Peer aufgerufen.

  • "call_local": Die Funktion kann auf dem lokalen Peer aufgerufen werden. Nützlich, wenn der Server auch ein Spieler ist.

transfer_mode:

  • "unreliable" Pakete werden nicht quittiert, können verloren gehen und in beliebiger Reihenfolge ankommen.

  • "unreliable_ordered" Pakete werden in der Reihenfolge empfangen, in der sie gesendet wurden. Dies wird dadurch erreicht, dass Pakete, die später eintreffen, ignoriert werden, wenn ein anderes, das nach ihnen gesendet wurde, bereits empfangen wurde. Kann bei unsachgemäßer Verwendung zu Paketverlusten führen.

  • "reliable" Wiederholte Sendeversuche werden gesendet, bis die Pakete bestätigt werden, und ihre Reihenfolge wird beibehalten. Hat einen erheblichen Performance-Verlust.

transfer_channel ist der Kanalindex.

Die ersten 3 können in beliebiger Reihenfolge übergeben werden, aber transfer_channel muss immer an letzter Stelle stehen.

Die Funktion multiplayer.get_remote_sender_id() kann verwendet werden, um die eindeutige ID eines rpc-Senders zu erhalten, wenn sie innerhalb der von rpc aufgerufenen Funktion verwendet wird.

func _on_some_input(): # Connected to some input.
    transfer_some_input.rpc_id(1) # Send the input only to the server.


# Call local is required if the server is also a player.
@rpc("any_peer", "call_local", "reliable")
func transfer_some_input():
    # The server knows who sent the input.
    var sender_id = multiplayer.get_remote_sender_id()
    # Process the input and affect game logic.

Kanäle

Moderne Netzwerkprotokolle unterstützen Kanäle, die separate Verbindungen innerhalb einer Verbindung darstellen. Dies ermöglicht mehrere Paketströme, die sich nicht gegenseitig stören.

Zum Beispiel sollten sowohl die Nachrichten, die sich auf den Spielchat beziehen, als auch einige der wichtigsten Gameplay-Nachrichten zuverlässig gesendet werden, aber eine Gameplay-Nachricht sollte nicht darauf warten, dass eine Chat-Nachricht bestätigt wird. Dies kann durch die Verwendung verschiedener Kanäle erreicht werden.

Kanäle sind auch nützlich, wenn sie mit dem unzuverlässigen geordneten Übertragungsmodus verwendet werden. Das Senden von Paketen unterschiedlicher Größe mit diesem Übertragungsmodus kann zu Paketverlusten führen, da Pakete, die langsamer ankommen, ignoriert werden. Die Aufteilung in mehrere Ströme homogener Pakete durch die Verwendung von Kanälen ermöglicht eine geordnete Übertragung mit geringen Paketverlusten und ohne die durch den zuverlässigen Modus verursachte Latenzzeit.

Der Default-Kanal mit dem Index 0 besteht eigentlich aus drei verschiedenen Kanälen - einer für jeden Übertragungsmodus.

Beispiel einer Lobby-Implementierung

Dies ist ein Beispiel für eine Lobby, die den Beitritt und das Verlassen von Peers verarbeiten kann, die UI-Szenen durch Signale benachrichtigt und das Spiel startet, nachdem alle Clients die Spielszene geladen haben.

extends Node

# Autoload named Lobby

# These signals can be connected to by a UI lobby scene or the game scene.
signal player_connected(peer_id, player_info)
signal player_disconnected(peer_id)
signal server_disconnected

const PORT = 7000
const DEFAULT_SERVER_IP = "127.0.0.1" # IPv4 localhost
const MAX_CONNECTIONS = 20

# This will contain player info for every player,
# with the keys being each player's unique IDs.
var players = {}

# This is the local player info. This should be modified locally
# before the connection is made. It will be passed to every other peer.
# For example, the value of "name" can be set to something the player
# entered in a UI scene.
var player_info = {"name": "Name"}

var players_loaded = 0



func _ready():
    multiplayer.peer_connected.connect(_on_player_connected)
    multiplayer.peer_disconnected.connect(_on_player_disconnected)
    multiplayer.connected_to_server.connect(_on_connected_ok)
    multiplayer.connection_failed.connect(_on_connected_fail)
    multiplayer.server_disconnected.connect(_on_server_disconnected)


func join_game(address = ""):
    if address.is_empty():
        address = DEFAULT_SERVER_IP
    var peer = ENetMultiplayerPeer.new()
    var error = peer.create_client(address, PORT)
    if error:
        return error
    multiplayer.multiplayer_peer = peer


func create_game():
    var peer = ENetMultiplayerPeer.new()
    var error = peer.create_server(PORT, MAX_CONNECTIONS)
    if error:
        return error
    multiplayer.multiplayer_peer = peer

    players[1] = player_info
    player_connected.emit(1, player_info)


func remove_multiplayer_peer():
    multiplayer.multiplayer_peer = null


# When the server decides to start the game from a UI scene,
# do Lobby.load_game.rpc(filepath)
@rpc("call_local", "reliable")
func load_game(game_scene_path):
    get_tree().change_scene_to_file(game_scene_path)


# Every peer will call this when they have loaded the game scene.
@rpc("any_peer", "call_local", "reliable")
func player_loaded():
    if multiplayer.is_server():
        players_loaded += 1
        if players_loaded == players.size():
            $/root/Game.start_game()
            players_loaded = 0


# When a peer connects, send them my player info.
# This allows transfer of all desired data for each player, not only the unique ID.
func _on_player_connected(id):
    _register_player.rpc_id(id, player_info)


@rpc("any_peer", "reliable")
func _register_player(new_player_info):
    var new_player_id = multiplayer.get_remote_sender_id()
    players[new_player_id] = new_player_info
    player_connected.emit(new_player_id, new_player_info)


func _on_player_disconnected(id):
    players.erase(id)
    player_disconnected.emit(id)


func _on_connected_ok():
    var peer_id = multiplayer.get_unique_id()
    players[peer_id] = player_info
    player_connected.emit(peer_id, player_info)


func _on_connected_fail():
    multiplayer.multiplayer_peer = null


func _on_server_disconnected():
    multiplayer.multiplayer_peer = null
    players.clear()
    server_disconnected.emit()

Der Root Node der Spielszene sollte den Namen Game tragen. In dem dazugehörigen Skript:

extends Node3D # Or Node2D.



func _ready():
    # Preconfigure game.

    Lobby.player_loaded.rpc_id(1) # Tell the server that this peer has loaded.


# Called only on the server.
func start_game():
    # All peers are ready to receive RPCs in this scene.

Exportieren für dedizierte Server

Sobald Sie ein Multiplayer-Spiel erstellt haben, möchten Sie es möglicherweise exportieren, um es auf einem dedizierten Server ohne verfügbare GPU auszuführen. Weitere Informationen finden Sie unter Exportieren für dedizierte Server.

Bemerkung

Die Codebeispiele auf dieser Seite können nicht auf einem dedizierten Server ausgeführt werden. Sie müssen sie ändern, damit der Server nicht als Spieler betrachtet wird. Sie müssen auch den Spielstartmechanismus ändern, damit der erste Spieler, der beitritt, das Spiel starten kann.