Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

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.

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.

This class interface can abstract most types of network layers, topologies and libraries. By default, Godot provides an implementation based on ENet (ENetMultiplayerPeer), one based on WebRTC (WebRTCMultiplayerPeer), and one based on WebSocket (WebSocketMultiplayerPeer), but this could be used to implement mobile APIs (for ad hoc WiFi, Bluetooth) or custom device/console-specific networking APIs.

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 networking in Godot is managed by the SceneTree.

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.

It is possible to create a new MultiplayerAPI object and assign it to a NodePath in the scene tree, which will override multiplayer for the node at that path and all of its descendants. This allows sibling nodes to be configured with different peers, which makes it possible to run a server and a client simultaneously in one instance of Godot.

# 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 = OfflineMultiplayerPeer.new()

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 will not serialize Objects or 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

Wenn eine Funktion im Client-Skript (bzw. Server-Skript) mit @rpc annotiert ist, dann muss diese Funktion auch im Server-Skript (bzw. Client-Skript) deklariert werden. Beide RPCs müssen die gleiche Signatur haben, die mit einer Prüfsumme von allen RPCs ausgewertet wird. Alle RPCs in einem Skript werden auf einmal geprüft, und alle RPCs müssen sowohl in den Client- als auch in den Server-Skripten deklariert werden, auch Funktionen, die gerade nicht verwendet werden.

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 are not checked for matching between the server and client code (example: func sendstuff(): and func sendstuff(arg1, arg2): will pass signature matching).

Wenn diese Bedingungen nicht erfüllt sind (wenn nicht alle RPCs den Signaturabgleich bestehen), kann das Skript einen Fehler ausgeben oder unerwünschtes Verhalten verursachen. Die Fehlermeldung kann in keinem Zusammenhang mit der RPC-Funktion stehen, die Sie gerade erstellen und testen.

Weitere Erklärungen und Fehlerbehebungen finden Sie in diesem Beitrag.

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

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

Die Parameter und ihre Funktionen sind wie folgt:

mode:

  • "authority": Only the multiplayer authority can call remotely. The authority is the server by default, but can be changed per-node using Node.set_multiplayer_authority.

  • "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.

Bemerkung

RPC methods must be defined on Node-derived classes. Attempting to use high-level RPC calls on methods defined only in non-Node classes (such as Resource) will result in runtime errors.

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 = OfflineMultiplayerPeer.new()
    players.clear()


# 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():
    remove_multiplayer_peer()


func _on_server_disconnected():
    remove_multiplayer_peer()
    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.

Authentication

Before hosting your game online to a public audience, you may want to consider adding authentication and protecting your RPCs against unauthenticated access. You can use the SceneMultiplayer's built-in authentication mechanism for this.

On the server:

# This goes after `multiplayer.multiplayer_peer = peer`.
multiplayer.auth_timout = 3
multiplayer.auth_callback = func(peer_id: int, payload: PackedByteArray):
    var auth_data: Dictionary = JSON.parse_string(payload.get_string_from_utf8())
    # Your authentication logic (such as checking the supplied username/password against a database)

    # Tell the MultiplayerAPI that the authentication was successful
    if authentication_successful:
        multiplayer.complete_auth(peer_id)

On the client:

# This goes after `multiplayer.multiplayer_peer = peer`.
multiplayer.auth_callback = func:
    # We have to set this on the client for the `peer_authenticating`
    # signal to emit.
    pass
multiplayer.peer_authenticating.connect(func(peer_id: int):
        var auth_data = {
            "username": "username",
            "password": "password",
        }
        multiplayer.send_auth(1, JSON.stringify(auth_data).to_utf8_buffer())

        # Tell the MultiplayerAPI that the authentication was successful.
        multiplayer.complete_auth(peer_id)

As soon as both the client's and the server's complete_auth() methods have been called, the connection is considered to be established and the connected_to_server and peer_connected signals fire.

Secure multiplayer design

Godot's high-level multiplayer API makes it easier to build networked games, but it does not automatically make gameplay logic secure. For competitive or persistent multiplayer games, treat all client input as untrusted.

A common mistake is to let clients authoritatively decide important game states, such as player position, combat results, inventory changes, or match outcomes. This can make cheating much easier, and result in more frequent desynchronization ("desync").

In general, prefer the following patterns:

  • Use server-authoritative logic for gameplay-critical decisions.

  • Validate RPC arguments before applying them to the game state.

  • Avoid trusting client-reported positions, timers, cooldowns, or resource values without checks.

  • Add safety checks and rate limits to actions that can be triggered frequently.

In short, you should design your networking so that the server remains the source of truth for important states.

For example, instead of accepting a client's final position directly, consider sending player input or movement intent to the authority/server, then validating and applying the result there. This comes with some tradeoffs (such as server-side performance and complexity due to the need for client-side prediction), but will make it much harder for attackers to cheat by sending falsified data.

See Choosing the right network model for your multiplayer game for more information on different multiplayer models and their security implications.