361 lines
9.9 KiB
Markdown
361 lines
9.9 KiB
Markdown
---
|
|
name: godot-networking
|
|
description: "Networking and multiplayer for Godot 4.x: Multiplayer API with RPC system, authority models (server-authoritative vs client-predictive), replication patterns (rpc(), rset(), rset_group()), connection management, lag compensation, and Steam Frame integration. Use when building multiplayer games, networked simulations, or online features in Godot."
|
|
license: MIT
|
|
compatibility:
|
|
- godot-4.0
|
|
- godot-4.1
|
|
- godot-4.2
|
|
- godot-4.3
|
|
- godot-4.4
|
|
- godot-4.5
|
|
- godot-4.6
|
|
- godot-4.7
|
|
metadata:
|
|
author: vl4dt
|
|
version: 0.1.0
|
|
tags:
|
|
- godot
|
|
- networking
|
|
- multiplayer
|
|
- rpc
|
|
- replication
|
|
- steam-frame
|
|
created: 2026-06-20
|
|
---
|
|
|
|
# Networking and Multiplayer for Godot 4.x
|
|
|
|
Comprehensive patterns for multiplayer, RPC communication, and network synchronization in Godot 4.x.
|
|
|
|
## Multiplayer API (Godot 4.x RPC System)
|
|
|
|
### Setting Up Multiplayer
|
|
|
|
```gdscript
|
|
# In _ready(), set the peer and peer ID
|
|
func _ready():
|
|
var peer = ENetMultiplayerPeer.new()
|
|
peer.create_client("127.0.0.1", 9000)
|
|
multiplayer.multiplayer_peer = peer
|
|
multiplayer.server_id = 1 # Server is always ID 1
|
|
```
|
|
|
|
### RPC Methods
|
|
|
|
```gdscript
|
|
# Remote: Call only from non-owner
|
|
@rpc("reliable", "call_remote")
|
|
func rpc_move_player(position: Vector3):
|
|
global_position = position
|
|
|
|
# Reliable RPC (ordered, guaranteed delivery)
|
|
@rpc("reliable", "authority", "call_remote")
|
|
func rpc_take_damage(amount: int, source_id: int):
|
|
health -= amount
|
|
if health <= 0:
|
|
rpc("rpc_die", source_id)
|
|
|
|
# Unreliable RPC (faster, may drop packets — good for position updates)
|
|
@rpc("unreliable", "authority", "call_remote")
|
|
func rpc_update_position(position: Vector3, rotation: float):
|
|
global_position = position
|
|
rotation_degrees = rotation
|
|
```
|
|
|
|
### RPC Reliability Options
|
|
|
|
| Mode | Use Case |
|
|
|------|----------|
|
|
| `"reliable"` | Damage, state changes, game events |
|
|
| `"unreliable"` | Position updates, player movement |
|
|
| `"unreliable_ordered"` | Ordered but fast (chat messages) |
|
|
| `"call_local"` | Only on the node owner |
|
|
| `"call_remote"` | On all non-owner nodes |
|
|
| `"call_authority"` | Only on the authority node |
|
|
## Authority Models
|
|
|
|
### Server-Authoritative Model
|
|
|
|
The server has final say on all game state. Clients send inputs, server validates and broadcasts results.
|
|
|
|
```gdscript
|
|
class_name ServerAuthPlayer extends CharacterBody3D
|
|
|
|
@export var player_id: int = 0
|
|
var _server := false
|
|
|
|
func _ready():
|
|
if multiplayer.is_server():
|
|
_server = true
|
|
set_multiplayer_authority(1)
|
|
else:
|
|
# Client-owned players are predicted locally
|
|
set_multiplayer_authority(multiplayer.get_unique_id())
|
|
|
|
# Client sends input to server
|
|
func _input(event: InputEvent) -> void:
|
|
if not multiplayer.is_server():
|
|
rpc("rpc_request_move", event)
|
|
|
|
# Server receives and validates input
|
|
@rpc("authority")
|
|
func rpc_request_move(event: InputEvent):
|
|
# Validate the input (check timing, etc.)
|
|
_process_input(event)
|
|
```
|
|
|
|
### Client-Predictive Model
|
|
|
|
Clients predict their own actions locally and correct on server acknowledgment.
|
|
|
|
```gdscript
|
|
class_name ClientPredictivePlayer extends CharacterBody3D
|
|
|
|
@export var prediction_enabled: bool = true
|
|
var _input_buffer := []
|
|
var _last_confirmed_input: int = 0
|
|
|
|
func _physics_process(delta: float):
|
|
# Apply buffered inputs up to current state
|
|
for input_data in _input_buffer:
|
|
if input_data.sequence > _last_confirmed_input:
|
|
_apply_input(input_data)
|
|
|
|
# Send position to server
|
|
rpc("rpc_update_position", global_position, rotation_degrees)
|
|
```
|
|
|
|
### When to Use Which
|
|
|
|
| Model | Best For |
|
|
|-------|----------|
|
|
| Server-authoritative | Competitive games, anti-cheat priority |
|
|
| Client-predictive | Fast-paced action, smooth feel priority |
|
|
| Hybrid (recommended) | Most multiplayer games |
|
|
## Replication Patterns
|
|
|
|
### rpc() — Remote Procedure Call
|
|
|
|
```gdscript
|
|
# Call a method on all other nodes
|
|
rpc("rpc_on_enemy_hit", damage, target_id)
|
|
|
|
# With parameters
|
|
@rpc("reliable")
|
|
func rpc_spawn_pickup(pickup_type: String, position: Vector3):
|
|
var pickup = preload("res://scenes/pickups/" + pickup_type + ".tscn").instantiate()
|
|
pickup.global_position = position
|
|
get_tree().root.add_child(pickup)
|
|
```
|
|
|
|
### rset() — Remote Set
|
|
|
|
Set a property on remote nodes.
|
|
|
|
```gdscript
|
|
# Set health bar on all clients
|
|
rset("health", new_health)
|
|
rset("max_health", max_health)
|
|
rset("visible", true)
|
|
|
|
# Bulk set with rset_group()
|
|
rset_group("players", "set_state", state_name)
|
|
```
|
|
|
|
### rset_group() — Broadcast to Group
|
|
|
|
```gdscript
|
|
# Add nodes to a group for batch updates
|
|
func _ready():
|
|
multiplayer.add_to_group("players")
|
|
|
|
# Update all players at once
|
|
func broadcast_position():
|
|
rset_group("players", "sync_position", global_position, rotation_degrees)
|
|
|
|
# Server-only group
|
|
func _ready():
|
|
if multiplayer.is_server():
|
|
add_to_group("server_only")
|
|
```
|
|
|
|
### Networked Autoload
|
|
|
|
Use autoloads for network-wide state.
|
|
|
|
```gdscript
|
|
# NetworkManager.gd (Autoload)
|
|
class_name NetworkManager extends Node
|
|
|
|
signal player_connected(player_id: int)
|
|
signal player_disconnected(player_id: int)
|
|
signal game_started()
|
|
|
|
var players := {}
|
|
|
|
func _ready():
|
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
|
|
|
func _on_peer_connected(id: int):
|
|
players[id] = PlayerInfo.new(id)
|
|
player_connected.emit(id)
|
|
|
|
func _on_peer_disconnected(id: int):
|
|
players.erase(id)
|
|
player_disconnected.emit(id)
|
|
```
|
|
## Connection Management
|
|
|
|
### Server Setup
|
|
|
|
```gdscript
|
|
class_name MultiplayerServer extends Node
|
|
|
|
const PORT = 9000
|
|
const MAX_PLAYERS = 16
|
|
var server: HTTPServer
|
|
|
|
func start_server():
|
|
var peer = ENetMultiplayerPeer.new()
|
|
peer.create_server(PORT, MAX_PLAYERS)
|
|
multiplayer.multiplayer_peer = peer
|
|
print("Server started on port ", PORT)
|
|
|
|
func stop_server():
|
|
if multiplayer.multiplayer_peer:
|
|
multiplayer.multiplayer_peer.disconnect_from_server()
|
|
```
|
|
|
|
### Client Connection
|
|
|
|
```gdscript
|
|
class_name MultiplayerClient extends Node
|
|
|
|
signal connected
|
|
signal connection_failed
|
|
|
|
func connect_to_server(host: String, port: int):
|
|
var peer = ENetMultiplayerPeer.new()
|
|
var error = peer.create_client(host, port)
|
|
if error != OK:
|
|
connection_failed.emit()
|
|
return
|
|
|
|
multiplayer.multiplayer_peer = peer
|
|
# Wait for server to assign unique ID
|
|
await get_tree().create_timer(0.5).timeout
|
|
if multiplayer.is_connected_to_server():
|
|
connected.emit()
|
|
```
|
|
|
|
### Lag Compensation
|
|
|
|
```gdscript
|
|
# Store player positions for rewind capability
|
|
class_name PositionHistory extends Node
|
|
|
|
@export var history_length: int = 3.0
|
|
var _history := []
|
|
|
|
func _physics_process(delta: float):
|
|
_history.push_back({
|
|
"position": global_position,
|
|
"rotation": rotation_degrees,
|
|
"time": Time.get_ticks_msec()
|
|
})
|
|
# Prune old entries
|
|
var cutoff = Time.get_ticks_msec() - int(history_length * 1000)
|
|
while _history.size() > 0 and _history[0].time < cutoff:
|
|
_history.pop_front()
|
|
|
|
func get_position_at(time_ms: int) -> Variant:
|
|
for entry in _history:
|
|
if entry.time >= time_ms:
|
|
return entry
|
|
return _history.back()
|
|
```
|
|
## Steam Frame Integration (Godot 4.7+)
|
|
|
|
### Using Steam Networking Sockets
|
|
|
|
```gdscript
|
|
# Requires Steamworks SDK and godot-steam plugin
|
|
func connect_steam_server(app_id: int, steam_id: String):
|
|
var peer = SteamMultiplayerPeer.new()
|
|
peer.create_client(steam_id)
|
|
multiplayer.multiplayer_peer = peer
|
|
```
|
|
|
|
### Frame-Synced Updates
|
|
|
|
For competitive games requiring frame-accurate sync:
|
|
|
|
```gdscript
|
|
@export var tick_rate: int = 60
|
|
var _tick_counter: int = 0
|
|
|
|
func _physics_process(delta: float):
|
|
_tick_counter += 1
|
|
if _tick_counter % (int(60 / tick_rate)) == 0:
|
|
rpc("rpc_tick_update", _tick_counter, get_state_snapshot())
|
|
```
|
|
|
|
### Steam Lobby Integration
|
|
|
|
```gdscript
|
|
func create_lobby(max_players: int = 4):
|
|
# Create lobby via Steamworks API
|
|
var lobby_id = Steam.matchmaking.create_lobby(
|
|
max_players,
|
|
Steam.LobbyType.LobbyPrivate
|
|
)
|
|
return lobby_id
|
|
|
|
func join_lobby(lobby_id: String):
|
|
Steam.matchmaking.join_lobby(lobby_id)
|
|
```
|
|
## Best Practices
|
|
|
|
### Do
|
|
- Use `@rpc("unreliable")` for frequent position updates — saves bandwidth
|
|
- Validate all client input on the server before applying
|
|
- Keep RPC payloads small — serialize only what changes
|
|
- Use `multiplayer.is_server()` guards to prevent client-side authority bugs
|
|
- Implement a heartbeat/ping system for connection quality monitoring
|
|
|
|
### Don't
|
|
- Don't call `rpc()` in `_process` without throttling — floods the network
|
|
- Don't trust client-reported health/damage — always recalculate server-side
|
|
- Don't send full state snapshots every frame — use delta compression
|
|
- Don't forget `set_multiplayer_authority()` when spawning remote nodes
|
|
- Don't mix reliable and unreliable RPCs for the same logical update
|
|
|
|
### Debugging Tips
|
|
```
|
|
# Enable network debugging in Project Settings:
|
|
network/enable_packet_logging = true
|
|
network/max_packet_size = 1400
|
|
|
|
# Print all RPC traffic:
|
|
multiplayer.peer_connected.connect(func(id): print("Peer connected: ", id))
|
|
multiplayer.peer_disconnected.connect(func(id): print("Peer disconnected: ", id))
|
|
```
|
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
|
|
|
When the MCP bridge is running (Phase 2), these tools enhance networking debugging:
|
|
|
|
- **`network_peer_list`** — List connected peers and their unique IDs
|
|
- **`rpc_trace`** — Log all RPC calls with sender/receiver info for debugging
|
|
- **`network_stats`** — View bandwidth usage, packet loss, and latency metrics
|
|
- **`lobby_inspect`** — Query Steam lobby state and player list
|
|
|
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
|
|
|
## References
|
|
|
|
- [Multiplayer API Documentation](references/multiplayer-api.md)
|
|
- [RPC Patterns Deep Dive](references/rpc-patterns.md)
|
|
- [Authority Models Guide](references/authority-models.md)
|