Skip to content

Instantly share code, notes, and snippets.

@Meshiest
Last active November 2, 2024 11:15
Show Gist options
  • Save Meshiest/1274c6e2e68960a409698cf75326d4f6 to your computer and use it in GitHub Desktop.
Save Meshiest/1274c6e2e68960a409698cf75326d4f6 to your computer and use it in GitHub Desktop.
Godot 4 Multiplayer Overview

Godot 4 Scene Multiplayer

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.

Base Knowledge

  • 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

Authority

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

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

mode

  • authority (default) - only the current authority of the target node can call this RPC.
  • any_peer - any peer can call this without permission
  • disabled - disable this RPC. convenient if you don't want to delete your code

sync

  • call_remote (default) - When called, the function will only be run on the remote peer
  • call_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.

transfer_mode

The docs cover this well. TL;DR: determines whether or not your packets are important and the transfer speed trade-off.

transfer_channel

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

Visibility determines two things:

  1. Whether a MultiplayerSynchronizer is active and replicating
  2. 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:

  1. If the StateSync is visible and the SpawnSync is not, the peer will still try to replicate player position to other peers
  2. If the SpawnSync is visible and the StateSync 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 MulitplayerSynchronizers 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.

Dedicated Servers

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.

@xaqbr
Copy link

xaqbr commented Sep 2, 2023

Some further clarification on multiplayer authority:

  • By default, clients can only use RPCs on nodes where their peer is set as the multiplayer authority. If the node script's RPC is configured with "any_peer", then any client can use that RPC.
  • MultiplayerSpawner only replicates nodes from its multiplayer authority client's scene tree, under spawn_path.
  • MultiplayerSynchronizer only replicates data from its multiplayer authority client's nodes.

@davidedistaso
Copy link

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)

@Meshiest
Copy link
Author

@davidedistaso

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)

I haven't played with this for a while but I have a demo that hides players from eachother based on a sphere trigger: https://github.com/Meshiest/godot-project-0

@Torkiwi
Copy link

Torkiwi commented Dec 11, 2023

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

Hey !
Did you find a solution for this?
Im currently trying to do something very similar

@comfortmfune
Copy link

May the good lord of rng bless your soul

I understand now. As someone with no real networking experience, it was horror before I found this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment