Multiplayer & Networking

Godot's high-level multiplayer is built on MultiplayerAPI (multiplayer property of every node, by default SceneMultiplayer) and a MultiplayerPeer transport:

33.1 Architecture

Godot's high-level multiplayer is built on MultiplayerAPI (multiplayer property of every node, by default SceneMultiplayer) and a MultiplayerPeer transport:

UDP (desktop/mobile) WebSockets (works in browsers) Default single-player stub Third-party (GDExtension) e.g., Steam networking

Each peer has a unique ID; the server is always ID 1. Every node has a multiplayer authority (default: server) that owns its state.

33.2 Hosting and joining

# network.gd (autoload)
extends Node

const PORT := 7000
const MAX_CLIENTS := 8

signal player_joined(id: int)
signal player_left(id: int)

func host() -> Error:
	var peer := ENetMultiplayerPeer.new()
	var err := peer.create_server(PORT, MAX_CLIENTS)
	if err != OK:
		return err
	multiplayer.multiplayer_peer = peer
	multiplayer.peer_connected.connect(func(id): player_joined.emit(id))
	multiplayer.peer_disconnected.connect(func(id): player_left.emit(id))
	player_joined.emit(1)               # the host is a player too
	return OK

func join(address: String) -> Error:
	var peer := ENetMultiplayerPeer.new()
	var err := peer.create_client(address, PORT)
	if err != OK:
		return err
	multiplayer.multiplayer_peer = peer
	multiplayer.connected_to_server.connect(func(): print("Connected as ", multiplayer.get_unique_id()))
	multiplayer.connection_failed.connect(func(): print("Connection failed"))
	multiplayer.server_disconnected.connect(func(): get_tree().change_scene_to_file("res://menu.tscn"))
	return OK

func leave() -> void:
	multiplayer.multiplayer_peer.close()
	multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()

33.3 RPCs

@rpc argument Options (default first) "authority" (only the authority may call), "any_peer"

"call_remote", "call_local" (also run on the caller) "unreliable", "unreliable_ordered", "reliable" integer (e.g., 1) to separate traffic

RPC functions must exist with the same node path and the same @rpc configuration on all peers.

@rpc("any_peer", "call_local", "reliable")
func send_chat(text: String) -> void:
	var sender := multiplayer.get_remote_sender_id()    # 0 when called locally
	$Chat.add_text("%d: %s\n" % [sender, text])

@rpc("authority", "call_remote", "unreliable_ordered")
func update_position(pos: Vector2) -> void:
	position = pos

@rpc("any_peer", "reliable")
func request_damage(target_path: NodePath, amount: int) -> void:
	if not multiplayer.is_server(): return
	# validate! never trust clients blindly
	var target := get_node_or_null(target_path)
	if target and amount <= 20:
		target.apply_damage(amount)

func _on_send_pressed() -> void:
	send_chat.rpc($LineEdit.text)                   # call on all peers (and locally, due to call_local)
	request_damage.rpc_id(1, enemy.get_path(), 10)  # call only on the server

33.4 Authority

# player.gd — the player node is named after its peer ID
func _enter_tree() -> void:
	set_multiplayer_authority(name.to_int())

func _physics_process(delta: float) -> void:
	if not is_multiplayer_authority():
		return                        # only the owning peer reads input
	velocity = Input.get_vector("left", "right", "up", "down") * 200.0
	move_and_slide()

33.5 MultiplayerSpawner and MultiplayerSynchronizer

MultiplayerSpawner: replicates node creation. Set spawn_path (the parent node) and add scenes to its *Auto Spawn List*. When the authority (server) adds a child of those scenes under spawn_path, clients get it too. For custom data use spawn_function + spawn(data).

MultiplayerSynchronizer: replicates properties (position, health, animation) from the authority to others. Configure properties in the *Replication* bottom panel (*Spawn*, *Sync*/*Replicate always or on change*), set replication_interval, and control visibility per peer.

# world.gd (server side)
const PlayerScene := preload("res://player.tscn")

func _ready() -> void:
	if multiplayer.is_server():
		Network.player_joined.connect(add_player)
		Network.player_left.connect(remove_player)

func add_player(id: int) -> void:
	var p := PlayerScene.instantiate()
	p.name = str(id)
	$Players.add_child(p, true)        # MultiplayerSpawner watching $Players replicates it

func remove_player(id: int) -> void:
	var p := $Players.get_node_or_null(str(id))
	if p: p.queue_free()

33.6 Practical notes

Test locally: *Debug → Customize Run Instances* (4.3+) to launch several instances; host on one, join 127.0.0.1 on others.

Use the server as the source of truth; clients send input, the server simulates, results are synchronized. Add client-side prediction and interpolation for fast-paced games.

Dedicated servers: export with the "Dedicated Server" option and run with --headless.

The Network Profiler in the debugger shows RPC and sync bandwidth.

Other networking: HTTPRequest/HTTPClient for REST APIs, StreamPeerTCP, PacketPeerUDP, WebSocketPeer.

Godot 3 note: remote, master, puppet, remotesync keywords and rpc("func_name") became @rpc(...) and func_name.rpc(); is_network_master() became is_multiplayer_authority(); NetworkedMultiplayerENet became ENetMultiplayerPeer.

func fetch_leaderboard() -> void:
	var http := HTTPRequest.new()
	add_child(http)
	http.request("https://example.com/api/scores")
	var result: Array = await http.request_completed   # [result, response_code, headers, body]
	if result[1] == 200:
		var data = JSON.parse_string(result[3].get_string_from_utf8())
		print(data)
	http.queue_free()