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.

Multiplayer di alto livello

API di alto livello vs. API di basso livello

Quanto segue spiega le differenze tra networking di alto e basso livello in Godot, oltre ad alcuni concetti fondamentali. Se vuoi cominciare subito ad aggiungere funzionalità di networking ai tuoi primi nodi, passa direttamente a Inizializzazione della rete qui sotto. Ma assicurati di leggere il resto più tardi!

Godot ha sempre supportato il networking standard di basso livello tramite UDP, TCP e alcuni protocolli di livello più alto come HTTP e SSL. Questi protocolli sono flessibili e possono servire praticamente per qualsiasi scopo. Tuttavia, utilizzarli per sincronizzare manualmente lo stato del gioco può richiedere molto lavoro. A volte questo lavoro è inevitabile o ne vale la pena, ad esempio quando si lavora con un'implementazione server personalizzata sul backend. Ma nella maggior parte dei casi, vale la pena considerare l'API di networking di alto livello di Godot, che sacrifica parte del controllo dettagliato del networking di basso livello in favore di una maggiore facilità d'uso.

Ciò è dovuto alle limitazioni intrinseche dei protocolli di basso livello:

  • TCP ensures packets will always arrive reliably and in order, but latency is generally higher due to error correction. It's also quite a complex protocol because it understands what a "connection" is, and optimizes for goals that often don't suit applications like multiplayer games. Packets are buffered to be sent in larger batches, trading less per-packet overhead for higher latency. This can be useful for things like HTTP, but generally not for games. Some of this can be configured and disabled (e.g. by disabling "Nagle's algorithm" for the TCP connection).

  • UDP is a simpler protocol, which only sends packets (and has no concept of a "connection"). No error correction makes it pretty quick (low latency), but packets may be lost along the way or received in the wrong order. Added to that, the MTU (maximum packet size) for UDP is generally low (only a few hundred bytes), so transmitting larger packets means splitting them, reorganizing them and retrying if a part fails.

In general, TCP can be thought of as reliable, ordered, and slow; UDP as unreliable, unordered and fast. Because of the large difference in performance, it often makes sense to re-build the parts of TCP wanted for games (optional reliability and packet order), while avoiding the unwanted parts (congestion/traffic control features, Nagle's algorithm, etc). Due to this, most game engines come with such an implementation, and Godot is no exception.

In summary, you can use the low-level networking API for maximum control and implement everything on top of bare network protocols or use the high-level API based on SceneTree that does most of the heavy lifting behind the scenes in a generally optimized way.

Nota

Most of Godot's supported platforms offer all or most of the mentioned high- and low-level networking features. As networking is always largely hardware and operating system dependent, however, some features may change or not be available on some target platforms. Most notably, the HTML5 platform currently offers WebSockets and WebRTC support but lacks some of the higher-level features, as well as raw access to low-level protocols like TCP and UDP.

Nota

More about TCP/IP, UDP, and networking: https://gafferongames.com/post/udp_vs_tcp/

Gaffer On Games has a lot of useful articles about networking in Games (here), including the comprehensive introduction to networking models in games.

Avvertimento

Adding networking to your game comes with some responsibility. It can make your application vulnerable if done wrong and may lead to cheats or exploits. It may even allow an attacker to compromise the machines your application runs on and use your servers to send spam, attack others or steal your users' data if they play your game.

This is always the case when networking is involved and has nothing to do with Godot. You can of course experiment, but when you release a networked application, always take care of any possible security concerns.

Mid-level abstraction

Before going into how we would like to synchronize a game across the network, it can be helpful to understand how the base network API for synchronization works.

Godot uses a mid-level object MultiplayerPeer. This object is not meant to be created directly, but is designed so that several C++ implementations can provide it.

This object extends from PacketPeer, so it inherits all the useful methods for serializing, sending and receiving data. On top of that, it adds methods to set a peer, transfer mode, etc. It also includes signals that will let you know when peers connect or disconnect.

Questa interfaccia di classe può astrarre la maggior parte dei tipi di livelli di rete, topologie e librerie. Godot fornisce di suo un'implementazione basata su ENet (ENetMultiplayerPeer), una basata su WebRTC (WebRTCMultiplayerPeer) e una basata su WebSocket (WebSocketMultiplayerPeer), ma si potrebbe usare per implementare API mobili (per Wi-Fi ad hoc, Bluetooth) o API di rete personalizzate specifiche per dispositivi/console.

For most common cases, using this object directly is discouraged, as Godot provides even higher level networking facilities. This object is still made available in case a game has specific needs for a lower-level API.

Hosting considerations

Quando si ospita un server, i client sulla propria LAN possono connettersi utilizzando l'indirizzo IP interno, che solitamente ha il formato 192.168.*.*. Questo indirizzo IP interno non è raggiungibile dai client non LAN/Internet.

On Windows, you can find your internal IP address by opening a command prompt and entering ipconfig. On macOS, open a Terminal and enter ifconfig. On Linux, open a terminal and enter ip addr.

If you're hosting a server on your own machine and want non-LAN clients to connect to it, you'll probably have to forward the server port on your router. This is required to make your server reachable from the Internet since most residential connections use a NAT. Godot's high-level multiplayer API only uses UDP, so you must forward the port in UDP, not just TCP.

After forwarding a UDP port and making sure your server uses that port, you can use this website to find your public IP address. Then give this public IP address to any Internet clients that wish to connect to your server.

Godot's high-level multiplayer API uses a modified version of ENet which allows for full IPv6 support.

Initializing the network

High-level networking in Godot is managed by the SceneTree.

Each node has a multiplayer property, which is a reference to the MultiplayerAPI instance configured for it by the scene tree. Initially, every node is configured with the same default MultiplayerAPI object.

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.

To initialize networking, a MultiplayerPeer object must be created, initialized as a server or client, and passed to the MultiplayerAPI.

# 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

To terminate networking:

multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()

Avvertimento

When exporting to Android, make sure to enable the INTERNET permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android.

Gestione delle connessioni

Every peer is assigned a unique ID. The server's ID is always 1, and clients are assigned a random positive integer.

Responding to connections or disconnections is possible by connecting to MultiplayerAPI's signals:

  • peer_connected(id: int) This signal is emitted with the newly connected peer's ID on each other peer, and on the new peer multiple times, once with each other peer's ID.

  • peer_disconnected(id: int) This signal is emitted on every remaining peer when one disconnects.

Il resto viene emesso solo sui client:

  • connected_to_server()

  • connection_failed()

  • server_disconnected()

To get the unique ID of the associated peer:

multiplayer.get_unique_id()

To check whether the peer is server or client:

multiplayer.is_server()

Chiamate di procedura remota

Le chiamate di procedura remota, o RPC, sono funzioni che possono essere chiamate su altri peer. Per crearne una, si utilizza l'annotazione @rpc prima della definizione della funzione. Per chiamare un'RPC, si utilizza il metodo rpc() di Callable per chiamarla su tutti i peer, oppure rpc_id() per chiamarla su un peer specifico.

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

Le RPC non serializzeranno gli Object o i Callable.

Affinché una chiamata remota abbia successo, il nodo mittente e quello ricevente devono avere lo stesso NodePath, il che significa anche lo stesso nome. Quando si utilizza add_child() per i nodi che si prevede utilizzino le RPC, imposta l'argomento force_readable_name su true.

Avvertimento

Se una funzione è annotata con @rpc nello script del client (risp. nello script del server), allora questa funzione deve essere dichiarata anche nello script del server (risp. nello script del client). Entrambe le chiamate RPC devono avere la stessa firma, che viene valutata tramite un checksum di tutte le chiamate RPC. Tutte le chiamate RPC in uno script vengono controllate alla volta, e tutte le chiamate RPC devono essere dichiarate sia negli script del client sia negli script del server, anche le funzioni che attualmente non sono in uso.

La firma dell'RPC include la dichiarazione @rpc(), la funzione, il tipo di ritorno e il NodePath. Se un RPC risiede in uno script allegato a /root/Main/Node1, deve risiedere esattamente nello stesso percorso e nodo sia nello script del client sia nello script del server. Gli argomenti delle funzioni non vengono verificati per la corrispondenza tra il codice del server e del client (esempio: func sendstuff(): e func sendstuff(arg1, arg2): supereranno la verifica di corrispondenza della firma).

Se queste condizioni non sono soddisfatte (se non tutte le chiamate RPC superano la corrispondenza della firma), lo script potrebbe stampare un errore o causare un comportamento indesiderato. Il messaggio di errore potrebbe non essere correlato alla funzione RPC che si sta attualmente creando e testando.

Per ulteriori spiegazioni e soluzioni a eventuali problemi, consultare questo post.

L'annotazione può accettare vari argomenti, che hanno valori predefiniti. @rpc è equivalente a:

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

I parametri e le loro funzioni sono i seguenti:

mode:

  • "authority": solo l'autorità multigiocatore può chiamare remotamente. L'autorità è il server come predefinito, ma si può cambiare per ogni nodo usando Node.set_multiplayer_authority.

  • "any_peer": i client sono autorizzati a chiamare remotamente. Utile per trasferire l'input dell'utente.

sync:

  • "call_remote": la funzione non verrà chiamata sul peer locale.

  • "call_local": la funzione può essere chiamata sul peer locale. Utile quando anche il server è un giocatore.

transfer_mode:

  • "unreliable" I pacchetti (inaffidabili) non vengono confermati, possono andare persi e possono arrivare in qualsiasi ordine.

  • "unreliable_ordered" I pacchetti (inaffidabili ordinati) vengono ricevuti nell'ordine in cui sono stati inviati. Lo si fa ignorando i pacchetti che arrivano in seguito se un altro pacchetto inviato dopo è già stato ricevuto. Può causare la perdita di pacchetti se utilizzato incorrettamente.

  • "reliable" Si effettuano più tentativi di reinvio finché i pacchetti non sono confermati, e il loro ordine è preservato. Ha un notevole impatto sulle prestazioni.

transfer_channel è l'indice di canale.

È possibile passare i primi 3 parametri in qualsiasi ordine, ma transfer_channel deve essere sempre l'ultimo.

La funzione multiplayer.get_remote_sender_id() serve per ottenere l'ID univoco di un mittente RPC, quando si utilizza all'interno della funzione chiamata da RPC.

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.

Nota

I metodi RPC devono essere definiti su classi derivate da Node. Tentare di usare chiamate RPC di alto livello su metodi definiti solo in classi non Node (come Resource) causerà errori in fase di esecuzione.

Canali

I protocolli moderni di rete supportano i canali, che sono connessioni separate all'interno della connessione principale. Ciò consente di gestire più flussi di pacchetti che non interferiscono tra loro.

Ad esempio, i messaggi relativi alla chat di gioco e alcuni dei messaggi principali del gioco si dovrebbero inviare in modo affidabile, ma un messaggio di gioco non dovrebbe attendere la conferma di un messaggio in chat. Ciò si può ottenere utilizzando canali diversi.

I canali sono utili anche quando utilizzati con la modalità di trasferimento ordinata inaffidabile. Inviare pacchetti di dimensioni variabili con questa modalità di trasferimento può causare la perdita di pacchetti, poiché i pacchetti che arrivano più lentamente vengono ignorati. Separarli in più flussi di pacchetti omogenei, tramite i canali, consente di trasferirli in modo ordinato con una perdita di pacchetti minima, e senza la penalità sulla latenza causata dalla modalità affidabile.

Il canale predefinito con indice 0 è in realtà composto da tre canali diversi, uno per ciascuna modalità di trasferimento.

Esempio di implementazione di una lobby

Questa è una lobby di esempio in grado di gestire l'ingresso e l'uscita dei peer, notificare le scene dell'interfaccia utente tramite segnali e avviare la partita dopo che tutti i client hanno caricato la scena di gioco.

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()

Il nodo radice della scena di gioco dovrebbe avere il nome Game. Nel suo script allegato:

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.

Esportare per i server dedicati

Una volta creato un gioco multigioacatore, potresti volerlo esportare per eseguirlo su un server dedicato senza GPU disponibile. Consulta la documentazione Esportare per i server dedicati per ulteriori informazioni.

Nota

Gli esempi di codice presenti in questa pagina non sono progettati per essere eseguiti su un server dedicato. Dovrai modificarli in modo che il server non sia considerato un giocatore. Dovrai anche modificare il meccanismo di avvio del gioco in modo che il primo giocatore che si connette possa avviare la partita.

Autenticazione

Prima di rendere il proprio gioco disponibile online a un pubblico, si potrebbe considerare di aggiungere un sistema di autenticazione e proteggere le RPC dagli accessi non autenticati. È possibile utilizzare il meccanismo di autenticazione integrato di SceneMultiplayer a tale scopo.

Sul 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)

Sul 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)

Non appena vengono chiamati entrambi i metodi complete_auth() del client e del server, la connessione viene considerata stabilita e vengono emessi i segnali connected_to_server e peer_connected.

Secure multiplayer design

L'API multigiocatore di alto livello di Godot rende più facile creare giochi in rete, ma non rende automaticamente sicura la logica di gioco. Per i giochi multigiocatore competitivi o persistenti, è bene considerare tutti gli input dei client come non attendibili.

Un errore comune è quello di lasciare che i client decidano autorevolmente gli stati di gioco più importanti, come la posizione dei giocatori, i risultati dei combattimenti, le modifiche all'inventario o l'esito delle partite. Questo può rendere molto più facile barare e risultare in desincronizzazioni più frequenti ("desync").

In generale, si consigliano i seguenti pattern:

  • Utilizzare una logica autorevole lato server per le decisioni critiche per il gameplay.

  • Verifica gli argomenti delle RPC prima di applicarli allo stato del gioco.

  • Evitare di fidarsi di posizioni, timer, cooldown o valori delle risorse riportati dai client senza effettuare verifiche.

  • Aggiungere verifiche di sicurezza e limiti di frequenza alle azioni che si possono attivare frequentemente.

In breve, è opportuno progettare il codice di rete in modo che il server rimanga la fonte di verità per gli stati importanti.

Ad esempio, invece di accettare direttamente la posizione finale di un client, si potrebbe considerare di inviare l'input del giocatore o l'intenzione di movimento all'autorità/server, per poi verificare e applicare il risultato lì. Questo comporta alcuni compromessi (come le prestazioni lato server e la complessità dovuta a dover effettuare predizioni lato client), ma renderà molto più difficile per gli aggressori barare inviando dati falsificati.

Consultare la pagina "Choosing the right network model for your multiplayer game" <https://mas-bandwidth.com/choosing-the-right-network-model-for-your-multiplayer-game/> per ulteriori informazioni sui diversi schemi multigiocatore e le relative implicazioni per la sicurezza.