Multiplayer Networking
多人游戏网络开发
Authoritative servers, client prediction, and state synchronization define robust multiplayer.
权威服务器、客户端预测和状态同步是构建稳定多人游戏的核心要素。
server_authoritative_controller.gd
server_authoritative_controller.gd
Advanced player controller with client prediction, server reconciliation, and interpolation.
集成客户端预测、服务器调和与插值功能的高级玩家控制器。
client_prediction_synchronizer.gd
client_prediction_synchronizer.gd
Expert client-side prediction with server reconciliation pattern.
NEVER Do in Multiplayer Networking
多人游戏网络开发绝对禁忌
- NEVER trust client input without server validation — Client sends "deal 9999 damage" RPC? Cheating. Server MUST validate actions:
if not multiplayer.is_server(): return
.
- NEVER use for gameplay actions — Any peer can call = cheating vector. Use for damage/spawns. Only for chat/cosmetics.
- NEVER use reliable RPCs for position updates — 60 position updates/sec with = bandwidth explosion + lag. Use for frequent, non-critical data.
- NEVER forget to set multiplayer authority — Both client and server process input? Desync. Call
set_multiplayer_authority(peer_id)
and check is_multiplayer_authority()
.
- NEVER sync every variable — MultiplayerSynchronizer syncing 50 properties = bandwidth waste. Only sync state OTHER clients need (skip animation frame, local UI state).
- NEVER block on — ENet is async. Calling
multiplayer.multiplayer_peer = peer
before server ready = crash. Await signal.
- NEVER forget interpolation for remote players — Teleporting remote players (direct position assignment) = jittery. Use to smooth between received positions.
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
- 绝对不要在没有服务器验证的情况下信任客户端输入 —— 客户端发送“造成9999点伤害”的RPC请求?这属于作弊行为。服务器必须验证所有操作:
if not multiplayer.is_server(): return
。
- 绝对不要将用于游戏核心操作 —— 任何节点都能调用会留下作弊漏洞。伤害计算、生成单位等操作请使用。仅在聊天、外观设置等非核心功能中使用。
- 绝对不要使用可靠RPC同步位置更新 —— 每秒60次位置更新使用会导致带宽爆炸和延迟。频繁的非关键数据请使用。
- 绝对不要忘记设置多人游戏权限 —— 客户端和服务器同时处理输入会导致不同步。调用
set_multiplayer_authority(peer_id)
并检查is_multiplayer_authority()
。
- 绝对不要同步所有变量 —— MultiplayerSynchronizer同步50个属性会造成带宽浪费。仅同步其他客户端需要的状态(跳过动画帧、本地UI状态等无需同步的内容)。
- 绝对不要在处阻塞线程 —— ENet是异步的。在服务器准备完成前设置
multiplayer.multiplayer_peer = peer
会导致崩溃。请等待信号。
- 绝对不要忘记对远程玩家使用插值 —— 直接赋值位置会导致远程玩家出现瞬移抖动。使用平滑过渡接收到的位置。
权威服务器模式:
- 服务器验证所有游戏状态
- 客户端发送输入,接收状态
- 防止作弊
点对点模式:
- 玩家直接连接
- 适用于小规模玩家数量
- 无需专用服务器
Create Multiplayer Peer
创建多人游戏节点
gdscript
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)
gdscript
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)
gdscript
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")
gdscript
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)
远程过程调用(RPC)
gdscript
extends CharacterBody2D
gdscript
extends CharacterBody2D
Called on all peers
Called on all peers
@rpc("any_peer", "call_local")
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
die()
@rpc("any_peer", "call_local")
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
die()
Usage: Call on specific peer
Usage: Call on specific peer
take_damage.rpc_id(1, 50) # Call on server (ID 1)
take_damage.rpc(50) # Call on all peers
take_damage.rpc_id(1, 50) # Call on server (ID 1)
take_damage.rpc(50) # Call on all peers
Only server can call, runs on all clients
Only server can call, runs on all clients
@rpc("authority", "call_remote")
func server_spawn_enemy(pos: Vector2) -> void:
pass
@rpc("authority", "call_remote")
func server_spawn_enemy(pos: Vector2) -> void:
pass
Any peer can call, runs locally too
Any peer can call, runs locally too
@rpc("any_peer", "call_local")
func player_chat(message: String) -> void:
pass
@rpc("any_peer", "call_local")
func player_chat(message: String) -> void:
pass
Reliable (TCP-like) vs Unreliable (UDP-like)
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:
pass
@rpc("any_peer", "call_local", "reliable")
func important_event() -> void:
pass
@rpc("any_peer", "call_local", "unreliable")
func position_update(pos: Vector2) -> void:
pass
MultiplayerSpawner
MultiplayerSpawner
Add MultiplayerSpawner node
Add MultiplayerSpawner node
Set spawn path and scenes
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 player
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 player
MultiplayerSynchronizer
MultiplayerSynchronizer
Add to synchronized node
Add to synchronized node
Set properties to sync
Set properties to sync
Scene structure:
Scene structure:
Player (CharacterBody2D)
Player (CharacterBody2D)
├─ MultiplayerSynchronizer
├─ MultiplayerSynchronizer
│ └─ Replication config:
│ └─ Replication config:
│ - position (sync)
│ - position (sync)
│ - velocity (sync)
│ - velocity (sync)
│ - health (sync)
│ - health (sync)
lobby_manager.gd (AutoLoad)
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)
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
状态同步
gdscript
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 = vel
gdscript
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 = vel
Check who owns this node
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 this
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 this
1. Validate on Server
1. 服务器端验证
gdscript
@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)
gdscript
@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
2. 频繁更新使用不可靠RPC
Position: unreliable (frequent)
Position: unreliable (frequent)
@rpc("any_peer", "unreliable")
func sync_position(pos: Vector2) -> void:
pass
@rpc("any_peer", "unreliable")
func sync_position(pos: Vector2) -> void:
pass
Damage: reliable (important)
Damage: reliable (important)
@rpc("authority", "reliable")
func apply_damage(amount: int) -> void:
pass
@rpc("authority", "reliable")
func apply_damage(amount: int) -> void:
pass
3. Interpolation for Smooth Movement
3. 插值实现平滑移动
gdscript
var target_position: Vector2
func _process(delta: float) -> void:
if not is_multiplayer_authority():
position = position.lerp(target_position, 15.0 * delta)
gdscript
var target_position: Vector2
func _process(delta: float) -> void:
if not is_multiplayer_authority():
position = position.lerp(target_position, 15.0 * delta)
- Master Skill: godot-master