I'm too lazy to write this as official documentation so I'm transcribing my experiences here for reference.
This is high level and does not cover how to setup your peer, only how to use the API itself.
This is not a tutorial.
If you are just getting started, this tutorial by DevLogLogan is worth watching.
- MultiplayerApi, is
multiplayer
in-game - Host/Server - the server owner
- Client - a player that joins the server
- Peer - any player, host, server or client, on the network
- Peer id - a unique number each peer is assigned
- Obtain your peer id with
multiplayer.get_unique_id()
- Within the scope of an rpc function,
multiplayer.get_remote_sender_id()
- The host will always have peer id of 1
- Obtain your peer id with
Authority in Godot 4 determines who controls a node. Without authority RPC calls or multiplayer functions will fail, sometimes without throwing any errors.
Authority is passed from one node to another by Node.set_multiplayer_authority(peer_id)
. You can use Node.get_multiplayer_authority()
or Node.is_multiplayer_authority()
to determine which peer currently has authority.
Every node by default is under the authority of the host. It's best to grant authority before Node._ready()
(through Node._enter_tree()
or as the node is created) and to make sure authority changes happen on all peers at the same time.
RPC can be enabled on methods by using the @rpc
annotation
The following options can be passed to it:
"mode"
or which peers can call it"sync"
or where the RPC is called"transfer_mode"
or how the packets can get to the peer"transfer_channel"
kind of like which car lane your RPC packets drive in.
The default values for @rpc
are as follows (the two below are the same):
@rpc
func fn(): pass
@rpc("authority", "call_remote", "unreliable", 0)
func fn_default(): pass
RPC can be called in a few ways but I prefer it as follows:
@rpc("any_peer")
func my_function(i: int) pass
@rpc("call_local")
func my_string_function(s: String) pass
func _ready():
# run this RPC on whichever peer has authority
my_function.rpc(5)
$SomeNode.my_function.rpc(5)
# run this RPC on the host
$SomeNode.my_string_function.rpc_id(1, "hello, world")
authority
(default) - only the current authority of the target node can call this RPC.any_peer
- any peer can call this without permissiondisabled
- disable this RPC. convenient if you don't want to delete your code
call_remote
(default) - When called, the function will only be run on the remote peercall_local
- When called, the function will be run on both the remote peer AND the peer calling the rpc
Note: If your peer is the host and you call a "call_remote"
RPC on your own peer, it will not run the code. For this, "call_local"
is helpful.
The docs cover this well. TL;DR: determines whether or not your packets are important and the transfer speed trade-off.
The docs cover this well. TL;DR: if you send a lot of packets on a specific channel, that channel can get congested and lose packets/get slowed down.
This is how nodes synchronize state between peers. You can specify whether or not a property is synchronized at spawn time or repeatedly through the Replication tab in the interface.
The spawner itself must have authority in order to replicate properties. The node at its root_path
doesn't have to be under the same authority.
This is how nodes get replicated to other peers. Adding child nodes to the node at spawner's spawn_path
, if present in the spawnable scenes list, will automatically replicate them to other players.
When spawning players or things with player-based authority, I prefer to use the spawn_function
feature and the following pattern:
## skeleton structure of this scene (ignoring things like camera/mesh/collision shape):
## Node
## -- CharacterBody
## ----- MultiplayerSynchronizer named "StateSync" (for moving the player)
## -- MultiplayerSynchronizer named "SpawnSync" (If you want to hide players from eachother), node_path MUST be the root node for visibility to work
##
const PlayerScene = preload("res://path/to/player.tscn")
## Initialize the spawner
func _enter_tree() -> void:
var spawner := $MultiplayerSpawner
spawner.spawn_function = _spawn_player
func _spawn_player(id: int):
var player := PlayerScene.instantiate()
# Rather than changing the authority of the player itself,
# change the body and its children (recursively)
# to allow the player's position to be synchronized
# but not the visibility
player.get_node("CharacterBody3D").set_multiplayer_authority(id)
player.peer_id = id # I like to also store this on players
return player
## Called when a peer connects or the non-dedicated server is created
func add_player(id: int) -> void:
# The id here will get sent to other peers on spawn automatically
$MultiplayerSpawner.spawn(id)
Visibility determines two things:
- Whether a
MultiplayerSynchronizer
is active and replicating - Whether a node is spawned on other players's clients
Visibility is interesting because if you don't have authority over the MultiplayerSynchronizer
, it is ignored. This is important because if you're trying to hide nodes on other clients while letting them also have authority over the synchronizer, the server will not change the visibility of the node.
In my example in the MultiplayerSpawner
section, I have two separate synchronizers. The one called SpawnSync
has host authority, while the one called StateSync
has peer authority.
Visibility filters or settings need to be applied to both of these for the same reasons as above:
- If the
StateSync
is visible and theSpawnSync
is not, the peer will still try to replicate player position to other peers - If the
SpawnSync
is visible and theStateSync
is not, the players will spawn but not move.
node_path
only needs to be the player scene root for the SpawnSync
because that is how the MultiplayerSpawner
knows where to get the visibility rules.
If public visibility is off and none of the MulitplayerSynchronizer
s have a node_path
the same as the root of one of the nodes spawned by the MultiplayerSpawner
, the nodes will be visibile regardless!
I like to use the Area3D
's area_exited
and area_entered
signals along with collision masks to only apply to players on the host to rpc to the peers whether or not players should be visible.
You can determine if the current instance is a dedicated server using DisplayServer.get_name() == "headless"
Dedicated server peers still have id of 1 even if they don't have any player node.
Would you be able to provide a simple example showing a node getting hidden and shown only for a certain client? I am trying to hide a node from all clients except for one. With public visibility disabled, I can get the node to spawn on the other client with MultiplayerSynchronizer.set_visibility_for(other_peer_id, true) but the node does not despawn from the previous client if I do MultiplayerSynchronizer.set_visibility_for(my_peer_id, false)