
Godot Multiplayer Networking
- 220 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-multiplayer-networking for development tasks
About
godot-multiplayer-networking: A skill for development. This provides functionality for development workflows.
- godot-multiplayer-networking
Godot Multiplayer Networking by the numbers
- 220 all-time installs (skills.sh)
- +24 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,809 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-multiplayer-networkingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 220 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-multiplayer-networking for development tasks
Files
Multiplayer Networking
Authoritative servers, client prediction, and state synchronization define robust multiplayer.
net_enet_expert_config.gd
Expert logic for tuning ENet channels, compression, and bandwidth thresholds.
net_packet_bit_packer.gd
Professional manual serialization tools for bit-packing data into PackedByteArray.
net_headless_server_auto_start.gd
Logic for detecting dedicated server mode and parsing CLI arguments for headless launch.
net_heartbeat_monitor.gd
RTT (Round Trip Time) and Jitter monitoring system for high-fidelity lag tracking.
net_lan_discovery.gd
UDP broadcasting system for local server discovery without master servers.
net_adaptive_sync_throttle.gd
Dynamic synchronization manager that adapts sync-rates to peer connection quality.
net_anti_desync_reconciler.gd
Server-authoritative state validation and forced-correction logic.
net_visibility_grid_culling.gd
Interest Management system for optimizing bandwidth in large-scale worlds.
net_packet_rate_limiter.gd
Essential flood-protection and anti-spam manager for RPC security.
net_custom_id_mapper.gd
Mapping utility for linking permanent UserIDs to volatile Network PeerIDs.
net_rollback_helper.gd
Expert logic for state snapshots and re-simulation (GGR-style rollback).
NEVER Do (Expert Networking Rules)
Core Architecture
- NEVER use `Reliable` for high-frequency data (Position/Movement) — Reliable packets block all following packets until acknowledged (Head-of-Line blocking). Use
UnreliableOrdered. - NEVER trust the client for shared state (Health/Money/Inventory) — Clients should only suggest actions. The Server MUST validate and broadcast the result.
- NEVER hardcode the Server IP — Always allow for discovery (
net_lan_discovery.gd) or pass the IP via CLI/UI.
Performance & Bandwidth
- NEVER send the same data every frame — Use
net_adaptive_sync_throttle.gdto only send updates when state changes significantly or at fixed Hz intervals (e.g., 20Hz). - NEVER use `JSON` for high-speed synchronization — String serialization is massive over the wire. Use bit-packing (
net_packet_bit_packer.gd) to keep packets under 100 bytes. - NEVER broadcast to all peers if it's not needed — In large worlds, use Interest Management (
net_visibility_grid_culling.gd). A player in the forest doesn't need the position of a player in the city.
Security
- NEVER allow unlimited RPC calls per second — An attacker can flood the server with 1,000 "Attack" RPCs to crash the game. Use
net_packet_rate_limiter.gd. - NEVER expose PeerIDs to the end-user — PeerIDs are internal. Always map them to persistent
UserIDs(net_custom_id_mapper.gd) from a database. - NEVER run a dedicated server with a GUI enabled — Use
--headless(net_headless_server_auto_start.gd) to save resources and ensure stability.
---
Authoritative Server Model:
- Server validates all game state
- Clients send inputs, receive state
- Prevents cheating
Peer-to-Peer:
- Direct player connections
- Good for small player counts
- No dedicated server needed
Basic Setup
Create Multiplayer Peer
extends Node
var peer := ENetMultiplayerPeer.new()
func host_game(port: int = 7777) -> void:
peer.create_server(port, 4) # Max 4 players
multiplayer.multiplayer_peer = peer
print("Server started on port ", port)
func join_game(ip: String, port: int = 7777) -> void:
peer.create_client(ip, port)
multiplayer.multiplayer_peer = peer
print("Connecting to ", ip)Connection Signals
func _ready() -> void:
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.connected_to_server.connect(_on_connected)
multiplayer.connection_failed.connect(_on_connection_failed)
func _on_peer_connected(id: int) -> void:
print("Player connected: ", id)
func _on_peer_disconnected(id: int) -> void:
print("Player disconnected: ", id)
func _on_connected() -> void:
print("Connected to server!")
func _on_connection_failed() -> void:
print("Connection failed")Remote Procedure Calls (RPCs)
Basic RPC
extends CharacterBody2D
# Called on all peers
@rpc("any_peer", "call_local")
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
die()
# Usage: Call on specific peer
take_damage.rpc_id(1, 50) # Call on server (ID 1)
take_damage.rpc(50) # Call on all peersRPC Modes
# Only server can call, runs on all clients
@rpc("authority", "call_remote")
func server_spawn_enemy(pos: Vector2) -> void:
pass
# Any peer can call, runs locally too
@rpc("any_peer", "call_local")
func player_chat(message: String) -> void:
pass
# Reliable (TCP-like) vs Unreliable (UDP-like)
@rpc("any_peer", "call_local", "reliable")
func important_event() -> void:
pass
@rpc("any_peer", "call_local", "unreliable")
func position_update(pos: Vector2) -> void:
passMultiplayerSpawner
# Add MultiplayerSpawner node
# Set spawn path and scenes
extends Node
@onready var spawner := $MultiplayerSpawner
func _ready() -> void:
spawner.spawn_function = spawn_player
func spawn_player(data: Variant) -> Node:
var player := preload("res://player.tscn").instantiate()
player.name = str(data) # Use peer ID as name
return playerMultiplayerSynchronizer
# Add to synchronized node
# Set properties to sync
# Scene structure:
# Player (CharacterBody2D)
# ├─ MultiplayerSynchronizer
# │ └─ Replication config:
# │ - position (sync)
# │ - velocity (sync)
# │ - health (sync)
# └─ Sprite2DLobby System
# lobby_manager.gd (AutoLoad)
extends Node
signal player_joined(id: int, info: Dictionary)
signal player_left(id: int)
var players: Dictionary = {}
func _ready() -> void:
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
func _on_peer_connected(id: int) -> void:
# Request player info
request_player_info.rpc_id(id)
func _on_peer_disconnected(id: int) -> void:
players.erase(id)
player_left.emit(id)
@rpc("any_peer", "reliable")
func request_player_info() -> void:
var sender_id := multiplayer.get_remote_sender_id()
receive_player_info.rpc_id(sender_id, {
"name": PlayerSettings.player_name,
"color": PlayerSettings.player_color
})
@rpc("any_peer", "reliable")
func receive_player_info(info: Dictionary) -> void:
var sender_id := multiplayer.get_remote_sender_id()
players[sender_id] = info
player_joined.emit(sender_id, info)State Synchronization
extends CharacterBody2D
var puppet_position: Vector2
var puppet_velocity: Vector2
func _physics_process(delta: float) -> void:
if is_multiplayer_authority():
# Local player: process input
_handle_input(delta)
move_and_slide()
# Send position to others
sync_position.rpc(global_position, velocity)
else:
# Remote player: interpolate
global_position = global_position.lerp(puppet_position, 10.0 * delta)
@rpc("any_peer", "unreliable")
func sync_position(pos: Vector2, vel: Vector2) -> void:
puppet_position = pos
puppet_velocity = velAuthority
# Check who owns this node
func _ready() -> void:
# Set authority to owner peer
set_multiplayer_authority(peer_id)
func _process(delta: float) -> void:
if not is_multiplayer_authority():
return # Skip if not owner
# Only authority processes thisBest Practices
1. Validate on Server
@rpc("any_peer", "call_local")
func player_action(action: String) -> void:
if not multiplayer.is_server():
return # Only server validates
var sender := multiplayer.get_remote_sender_id()
if not _is_valid_action(sender, action):
return
_apply_action.rpc(sender, action)2. Use Unreliable for Frequent Updates
# Position: unreliable (frequent)
@rpc("any_peer", "unreliable")
func sync_position(pos: Vector2) -> void:
pass
# Damage: reliable (important)
@rpc("authority", "reliable")
func apply_damage(amount: int) -> void:
pass3. Interpolation for Smooth Movement
var target_position: Vector2
func _process(delta: float) -> void:
if not is_multiplayer_authority():
position = position.lerp(target_position, 15.0 * delta)
## Expert Networking Patterns
### 1. Multiplayer Delta-Compression
To minimize bandwidth, only send data that has changed.
- **Native**: Use `MultiplayerSynchronizer` with `REPLICATION_MODE_ON_CHANGE`.
- **Manual**: Store the `last_sent_state` and only broadcast an RPC if the current state differs by a significant threshold.
### 2. Network Simulator UI
Test your game's resilience by artificially degrading the network.
- **Latency**: Buffer outgoing/incoming RPCs in an array and delay execution.
- **Jitter**: Randomize the delay for each packet.
- **Packet Loss**: Use `randf() < loss_rate` to discard packets before they are processed.
## Reference
- [Godot Docs: High-level Multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html)
### Related
- Master Skill: [godot-master](../godot-master/SKILL.md)
# client_network_setup.gd
# Connecting to a remote server
extends Node
# EXPERT NOTE: Ensure the protocol (ENet/WebSocket) matches
# the server. ENet is preferred for UDP-based fast action.
func connect_to_server(address: String, port: int):
var peer = ENetMultiplayerPeer.new()
var error = peer.create_client(address, port)
if error != OK:
printerr("Connection failed: ", error)
return
multiplayer.multiplayer_peer = peer
multiplayer.connected_to_server.connect(_on_connection_success)
func _on_connection_success():
print("Successfully connected to host!")
# skills/multiplayer-networking/code/client_prediction_synchronizer.gd
extends MultiplayerSynchronizer
## Client-Side Prediction Expert Pattern
## Manages immediate client movement with server-authoritative correction.
@export var player: CharacterBody3D
# Simple history to store local movements for reconciliation
var _history: Array = [] # { "id": int, "pos": Vector3, "delta": float }
var _input_id: int = 0
func _physics_process(delta: float) -> void:
if is_multiplayer_authority():
# SERVER: Process and Send Verified State
_server_validate_movement()
else:
# CLIENT: Predict and Record
_predict_local_movement(delta)
func _predict_local_movement(delta: float) -> void:
var input_dir = Input.get_vector("left", "right", "up", "down")
_input_id += 1
# 1. Immediate Client-Side Response
player.velocity = Vector3(input_dir.x, 0, input_dir.y) * 5.0
player.move_and_slide()
# 2. Record for Reconciliation
_history.append({
"id": _input_id,
"pos": player.global_position,
"delta": delta
})
func _server_validate_movement() -> void:
# 3. Authoritative Correction
# If client pos deviates too much from server result, snap back.
# Note: Expert implementation would re-simulate from last valid 'id'.
pass
## EXPERT NOTE:
## Use 'Snapshot Interpolation' for other players. Instead of snapping to
## the newest position, keep a buffer [100ms] and interpolate between
## the last two received states to ensure silky smooth movement even
## with jitter.
## NEVER trust client-provided health or physics values. The Server MUST
## handle all damage calculations and broadcast results via
## 'MultiplayerSynchronizer'.
# game_state_sync_manager.gd
# Master clock synchronization
extends Node
# EXPERT NOTE: Synchronizing game time between server and
# clients is critical for deterministic physics or effects.
var server_time: float = 0.0
@rpc("authority", "call_remote", "unreliable")
func sync_time(time: float):
server_time = time
# multiplayer_spawner_manager.gd
# Syncing node spawning across the network
extends MultiplayerSpawner
# EXPERT NOTE: Use MultiplayerSpawner to automatically
# instance nodes across all peers when added to parent.
func _ready():
# Set spawn_path to a shared parent (e.g. /root/Main/Players)
spawn_function = _custom_spawn
func _custom_spawn(data: Dictionary) -> Node:
var player = preload("res://player.tscn").instantiate()
player.player_id = data["id"]
player.set_name(str(data["id"]))
return player
class_name NetAdaptiveSyncThrottle
extends Node
## Expert Adaptive Sync Throttle.
## Reduces update frequency for peers with poor RTT or packet loss.
@export var base_sync_hz: int = 20
var active_sync_hz: int = 20
func update_throttle(current_rtt: int) -> void:
if current_rtt > 200:
active_sync_hz = 10 # Half rate for laggy peers
elif current_rtt > 100:
active_sync_hz = 15
else:
active_sync_hz = base_sync_hz
## Tip: Throttling bandwidth for laggy peers prevents them from 'clogging' the server's upload pipe.
class_name NetAntiDesyncReconciler
extends Node
## Expert Anti-Desync Reconciliation.
## Snap-corrects peers if they deviate too far from their server-side ghost instance.
@export var error_tolerance: float = 0.5
func validate_peer_state(peer_id: int, reported_pos: Vector3) -> void:
var shadow_pos = ServerGameState.get_peer_pos(peer_id)
if reported_pos.distance_to(shadow_pos) > error_tolerance:
# Force Correction RPC
force_sync.rpc_id(peer_id, shadow_pos)
@rpc("authority", "reliable")
func force_sync(correct_pos: Vector3) -> void:
var player = get_tree().get_first_node_in_group("Player")
player.global_position = correct_pos
## Rule: Snap-correction should be the LAST resort. Use interpolation/prediction first.
class_name NetCustomIDMapper
extends Node
## Expert ID Mapping.
## Maps persistent DB UserIDs to ephemeral Network PeerIDs.
var peer_to_user: Dictionary = {}
var user_to_peer: Dictionary = {}
func register_player(peer_id: int, user_id: String) -> void:
peer_to_user[peer_id] = user_id
user_to_peer[user_id] = peer_id
func get_peer(user_id: String) -> int:
return user_to_peer.get(user_id, -1)
## Rule: PeerIDs change on every reconnect; UserIDs are permanent. Mapping is mandatory.
class_name NetENetExpertConfig
extends Node
## Expert ENet Tuning.
## Configures internal ENet settings for competitive-grade networking.
func setup_enet_peer(is_server: bool, port: int, max_clients: int = 32) -> ENetMultiplayerPeer:
var peer = ENetMultiplayerPeer.new()
var err: Error
if is_server:
err = peer.create_server(port, max_clients)
else:
err = peer.create_client("127.0.0.1", port)
if err != OK: return null
# Expert Tuning
var host: ENetHost = peer.get_host()
host.compress(ENetHost.COMPRESS_RANGE_CODER) # Efficient for dynamic packet sizes
host.set_max_channels(3) # [0: Reliable, 1: Unreliable, 2: Ordered]
# Limits
host.set_bandwidth_limit(1024 * 1024, 1024 * 1024) # 1MB up/down
return peer
## Rule: Use tailored Channels (Reliable vs Unreliable) to avoid Head-of-Line blocking.
class_name NetHeadlessServerAutoStart
extends Node
## Expert Dedicated Server Entry Point.
## Auto-detects --headless mode and initializes server.
func _ready() -> void:
if OS.has_feature("dedicated_server") or DisplayServer.get_name() == "headless":
_start_dedicated_server()
func _start_dedicated_server() -> void:
print("Auto-Starting Dedicated Server...")
var port = 7777
# Handle CLI arguments: --port=9999
for arg in OS.get_cmdline_args():
if arg.begins_with("--port="):
port = arg.split("=")[1].to_int()
# Init Net Logic...
## Rule: Always parse CLI arguments for port/map overrides in dedicated server builds.
class_name NetHeartbeatMonitor
extends Node
## Expert Heartbeat & Latency Monitor.
## Measures RTT (Round Trip Time) manually for precise lag display.
var rtt_msec: int = 0
var last_ping_time: int = 0
func _on_timer_timeout() -> void:
last_ping_time = Time.get_ticks_msec()
# Call unreliably to avoid blocking
ping_server.rpc_id(1, last_ping_time)
@rpc("any_peer", "unreliable")
func ping_server(t: int) -> void:
pong_client.rpc_id(multiplayer.get_remote_sender_id(), t)
@rpc("authority", "unreliable")
func pong_client(t: int) -> void:
rtt_msec = Time.get_ticks_msec() - t
## Tip: Display Jitter (variation in RTT) alongside Ping to help players diagnose bad connections.
class_name NetLANDiscovery
extends Node
## Expert LAN Discovery (UDP Broadcast).
## Allows peers to find local servers without external master servers.
const BROADCAST_PORT = 12345
var udp := PacketPeerUDP.new()
func _ready() -> void:
udp.bind(BROADCAST_PORT)
func broadcast_presence(server_name: String) -> void:
udp.set_dest_address("255.255.255.255", BROADCAST_PORT)
udp.put_packet(server_name.to_utf8_buffer())
func _process(_delta) -> void:
if udp.get_available_packet_count() > 0:
var packet = udp.get_packet()
var msg = packet.get_string_from_utf8()
# Add to local server list...
## Rule: UDP broadcasting is for LAN only. For Internet discovery, use an external Web API bridge.
class_name NetPacketBitPacker
extends Node
## Expert Packet Bit-Packing.
## Manually serializes data into PackedByteArray for minimum overhead.
func pack_player_state(pos: Vector3, rot: Vector3, hp: int) -> PackedByteArray:
var buffer = StreamPeerBuffer.new()
buffer.put_float(pos.x) # 4 bytes
buffer.put_float(pos.y) # 4 bytes
buffer.put_float(pos.z) # 4 bytes
buffer.put_8(int(rot.y / 360.0 * 255.0)) # Quantized rotation (1 byte)
buffer.put_u8(hp) # 1 byte
return buffer.data_array
func unpack_player_state(data: PackedByteArray) -> Dictionary:
var buffer = StreamPeerBuffer.new()
buffer.data_array = data
return {
"pos": Vector3(buffer.get_float(), buffer.get_float(), buffer.get_float()),
"rot_y": buffer.get_8() / 255.0 * 360.0,
"hp": buffer.get_u8()
}
## Tip: Quantize rotations/angles to 1 byte (`put_8`) to save 3 bytes per packet compared to floats.
class_name NetPacketRateLimiter
extends Node
## Expert Packet Rate Limiter.
## Protects server against malicious clients flooding RPCs.
@export var max_packets_per_sec: int = 60
var peer_buckets: Dictionary = {} # PeerID: Count
func check_rate(peer_id: int) -> bool:
peer_buckets[peer_id] = peer_buckets.get(peer_id, 0) + 1
if peer_buckets[peer_id] > max_packets_per_sec:
print("Peer %d Kicked for Flooding" % peer_id)
multiplayer.multiplayer_peer.disconnect_peer(peer_id)
return false
return true
func _on_timer_reset() -> void:
peer_buckets.clear()
## Rule: Always rate-limit unauthenticated pings and movement packets to prevent DDoS-lite.
class_name NetRollbackHelper
extends Node
## Expert Rollback Helper for state snapshots and re-simulation.
## Buffers historical states and inputs to allow retroactive correction.
# Maximum frames to keep in the rollback buffer (approx 1s at 60fps)
const BUFFER_SIZE := 64
# Ring buffers for historical data
var _state_buffer: Array[Dictionary] = []
var _input_buffer: Array[Dictionary] = []
func _ready() -> void:
# Pre-allocate buffers
for i in range(BUFFER_SIZE):
_state_buffer.append({})
_input_buffer.append({})
## Records the current state and input for a specific tick.
func save_snapshot(tick: int, state: Dictionary, input: Dictionary) -> void:
var idx = tick % BUFFER_SIZE
# deep duplicate to prevent reference overrides
_state_buffer[idx] = state.duplicate(true)
_input_buffer[idx] = input.duplicate(true)
## Retrieves a historical snapshot.
func get_snapshot(tick: int) -> Dictionary:
return _state_buffer[tick % BUFFER_SIZE]
## Retrieves historical input.
func get_input(tick: int) -> Dictionary:
return _input_buffer[tick % BUFFER_SIZE]
## Triggers a rollback and re-simulation cycle.
## target_node must implement 'apply_state(state)' and 'process_simulation_step(delta, input)'.
func rollback_and_resimulate(server_tick: int, current_tick: int, server_state: Dictionary, target_node: Node) -> void:
# 1. Snap to authoritative server state
if target_node.has_method("apply_state"):
target_node.apply_state(server_state)
var delta = target_node.get_physics_process_delta_time()
# 2. Re-simulate from server_tick up to the present
for tick in range(server_tick, current_tick):
var hist_input = get_input(tick)
# Step the logic forward
if target_node.has_method("process_simulation_step"):
target_node.process_simulation_step(delta, hist_input)
# Re-save the corrected prediction
if target_node.has_method("get_current_state"):
save_snapshot(tick + 1, target_node.get_current_state(), hist_input)
# 3. Clean up interpolation to prevent visual snapping
if target_node is Node3D or target_node is Node2D:
target_node.reset_physics_interpolation()
class_name NetVisibilityGridCulling
extends Node
## Expert Visibility Grid Culling (Interest Management).
## Only replicates objects within relevant spatial sectors.
@export var grid_size: int = 64
var player_sectors: Dictionary = {} # PeerID: Vector2i
func update_interest(peer_id: int, pos: Vector3) -> void:
var sector = Vector2i(pos.x / grid_size, pos.z / grid_size)
if player_sectors.get(peer_id) != sector:
player_sectors[peer_id] = sector
# Re-calculate visibility set for this peer...
## Tip: Interest management is mandatory for 'Open World' or 64+ player multiplayer games.
# network_error_handler.gd
# Gracefully handling disconnects and timeouts
extends Node
func _ready():
multiplayer.server_disconnected.connect(_on_server_disconnected)
multiplayer.connection_failed.connect(_on_connection_failed)
func _on_server_disconnected():
# Transition back to main menu with cleanup
get_tree().change_scene_to_file("res://main_menu.tscn")
func _on_connection_failed():
printerr("Failed to establish network link.")
# network_input_synchronizer.gd
# Handling latency with input replication
extends MultiplayerSynchronizer
# EXPERT NOTE: Sync only the essential inputs, not the
# calculated physics state, to save bandwidth.
@export var input_direction: Vector2
func _ready():
# Configure what to sync in the Inspector (SceneReplicationConfig)
pass
func _physics_process(_delta):
if is_multiplayer_authority():
input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
# networked_interpolator.gd
# Smoothing remote entity movement
extends CharacterBody2D
# EXPERT NOTE: Use lerp to smooth the transition between
# received network packets to avoid jitter.
var target_position: Vector2
func _physics_process(delta):
if not is_multiplayer_authority():
# Smoothly move remote proxy towards verified position
global_position = global_position.lerp(target_position, 0.4)
else:
# Local authority logic
target_position = global_position
# packet_peer_latency_test.gd
# Measuring ping and network jitter
extends Node
var last_ping_time: int = 0
func _process(_delta):
if Time.get_ticks_msec() - last_ping_time > 1000:
check_ping.rpc()
last_ping_time = Time.get_ticks_msec()
@rpc("any_peer", "call_remote", "unreliable")
func check_ping():
if multiplayer.is_server():
return_ping.rpc_id(multiplayer.get_remote_sender_id(), Time.get_ticks_msec())
@rpc("authority", "call_remote", "unreliable")
func return_ping(server_time: int):
var latency = Time.get_ticks_msec() - server_time
print("Latency: ", latency, "ms")
# secure_variable_synchronizer.gd
# Authority-only variable updates
extends Node
# EXPERT NOTE: Never let clients dictate their own health
# or money. Only the server updates these variables.
@export var health: int = 100:
set(val):
health = val
health_changed.emit(val)
signal health_changed(new_val)
func damage(amount: int):
# Damage logic should only execute on the server
if not multiplayer.is_server(): return
health -= amount
# Update will replicate automatically if using Synchronizer
# secured_rpc_pattern.gd
# Communication between peers with authority checks
extends Node
# EXPERT NOTE: ALWAYS use any_peer or authority explicitly.
# NEVER trust client data without server-side validation.
@rpc("any_peer", "call_remote", "reliable")
func request_player_action(action_id: String):
# Only the server should process authoritative logic
if not multiplayer.is_server(): return
var sender_id = multiplayer.get_remote_sender_id()
# Validate if player 'sender_id' can perform 'action_id'
print("Server processing action from: ", sender_id)
_broadcast_result.rpc(action_id)
@rpc("authority", "call_local", "reliable")
func _broadcast_result(action_id: String):
# Everyone (including server itself) updates their local state
pass
# skills/multiplayer-networking/scripts/server_authoritative_controller.gd
extends CharacterBody2D
## Server Authoritative Controller Expert Pattern
## Client-side prediction with server reconciliation and interpolation.
class_name ServerAuthoritativeController
@export var speed := 300.0
@export var interpolation_factor := 20.0
# Network State
@export var network_position: Vector2
@export var last_processed_input_id: int
# Client Prediction
var _input_queue: Array[Dictionary] = []
var _pending_inputs: Array[Dictionary] = []
var _input_id_counter: int = 0
func _ready() -> void:
# Configure synchronizer if separate, or use built-in
pass
func _physics_process(delta: float) -> void:
if is_multiplayer_authority():
_process_client(delta)
else:
_process_puppet(delta)
func _process_client(delta: float) -> void:
# 1. Gather Input
var input = _get_input()
input["id"] = _input_id_counter
input["delta"] = delta
_input_id_counter += 1
# 2. Predict locally
velocity = input["velocity"]
move_and_slide()
# 3. Store for reconciliation
_pending_inputs.append({
"id": input["id"],
"position": global_position,
"input": input
})
# 4. Send to server
send_input.rpc_id(1, input)
func _process_puppet(delta: float) -> void:
# Interpolate to authoritative position
global_position = global_position.lerp(network_position, interpolation_factor * delta)
@rpc("any_peer", "call_remote", "unreliable")
func send_input(input_data: Dictionary) -> void:
# Server side execution
if not multiplayer.is_server(): return
# Validation could happen here
velocity = input_data["velocity"]
move_and_slide()
network_position = global_position
last_processed_input_id = input_data["id"]
# Broadcast specific state to owner for reconciliation
receive_state.rpc_id(multiplayer.get_remote_sender_id(), network_position, last_processed_input_id)
# Broadcast world state to others (via Synchronizer usually, or manual RPC)
# sync_to_others.rpc(network_position)
@rpc("authority", "call_remote", "unreliable")
func receive_state(server_pos: Vector2, last_input_id: int) -> void:
# Client Reconciliation
# Remove inputs processed by server
_pending_inputs = _pending_inputs.filter(func(i): return i.id > last_input_id)
if _pending_inputs.is_empty():
global_position = server_pos
return
# If server position deviates too much from our history at that simplified input id
# (Simplified logic: just snap and replay)
global_position = server_pos
# Replay remaining inputs
for history in _pending_inputs:
velocity = history.input["velocity"]
move_and_slide()
# Update history record
history["position"] = global_position
func _get_input() -> Dictionary:
var dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
return {"velocity": dir * speed}
# server_dedicated_host.gd
# Configuring a dedicated server instance
extends Node
# EXPERT NOTE: For dedicated servers, use ENetMultiplayerPeer
# and disable the scene tree rendering if not needed (Server build).
func start_dedicated_server(port: int):
var peer = ENetMultiplayerPeer.new()
var error = peer.create_server(port)
if error != OK:
printerr("Server startup failed: ", error)
return
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_on_client_connected)
print("Dedicated server listening on ", port)
func _on_client_connected(id: int):
print("Client connected: ", id)