godot-multiplayer-networking

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Multiplayer Networking

多人游戏网络开发

Authoritative servers, client prediction, and state synchronization define robust multiplayer.
权威服务器、客户端预测和状态同步是构建稳定多人游戏的核心要素。

Available Scripts

可用脚本

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
    @rpc("any_peer")
    for gameplay actions
    — Any peer can call = cheating vector. Use
    @rpc("authority")
    for damage/spawns. Only
    any_peer
    for chat/cosmetics.
  • NEVER use reliable RPCs for position updates — 60 position updates/sec with
    "reliable"
    = bandwidth explosion + lag. Use
    "unreliable"
    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
    peer.create_server()
    — ENet is async. Calling
    multiplayer.multiplayer_peer = peer
    before server ready = crash. Await
    peer_connected
    signal.
  • NEVER forget interpolation for remote players — Teleporting remote players (direct position assignment) = jittery. Use
    lerp()
    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("any_peer")
    用于游戏核心操作
    —— 任何节点都能调用会留下作弊漏洞。伤害计算、生成单位等操作请使用
    @rpc("authority")
    。仅在聊天、外观设置等非核心功能中使用
    any_peer
  • 绝对不要使用可靠RPC同步位置更新 —— 每秒60次位置更新使用
    "reliable"
    会导致带宽爆炸和延迟。频繁的非关键数据请使用
    "unreliable"
  • 绝对不要忘记设置多人游戏权限 —— 客户端和服务器同时处理输入会导致不同步。调用
    set_multiplayer_authority(peer_id)
    并检查
    is_multiplayer_authority()
  • 绝对不要同步所有变量 —— MultiplayerSynchronizer同步50个属性会造成带宽浪费。仅同步其他客户端需要的状态(跳过动画帧、本地UI状态等无需同步的内容)。
  • 绝对不要在
    peer.create_server()
    处阻塞线程
    —— ENet是异步的。在服务器准备完成前设置
    multiplayer.multiplayer_peer = peer
    会导致崩溃。请等待
    peer_connected
    信号。
  • 绝对不要忘记对远程玩家使用插值 —— 直接赋值位置会导致远程玩家出现瞬移抖动。使用
    lerp()
    平滑过渡接收到的位置。

权威服务器模式:
  • 服务器验证所有游戏状态
  • 客户端发送输入,接收状态
  • 防止作弊
点对点模式:
  • 玩家直接连接
  • 适用于小规模玩家数量
  • 无需专用服务器

Basic Setup

基础设置

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)

Connection Signals

连接信号

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)

Basic RPC

基础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
undefined
take_damage.rpc_id(1, 50) # Call on server (ID 1) take_damage.rpc(50) # Call on all peers
undefined

RPC Modes

RPC模式

gdscript
undefined
gdscript
undefined

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
undefined
@rpc("any_peer", "call_local", "reliable") func important_event() -> void: pass
@rpc("any_peer", "call_local", "unreliable") func position_update(pos: Vector2) -> void: pass
undefined

MultiplayerSpawner

MultiplayerSpawner

gdscript
undefined
gdscript
undefined

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
undefined
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
undefined

MultiplayerSynchronizer

MultiplayerSynchronizer

gdscript
undefined
gdscript
undefined

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)

└─ Sprite2D

└─ Sprite2D

undefined
undefined

Lobby System

大厅系统

gdscript
undefined
gdscript
undefined

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

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

Authority

权限管理

gdscript
undefined
gdscript
undefined

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
undefined
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
undefined

Best Practices

最佳实践

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

gdscript
undefined
gdscript
undefined

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
undefined
@rpc("authority", "reliable") func apply_damage(amount: int) -> void: pass
undefined

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)

Reference

参考资料

Related

相关内容

  • Master Skill: godot-master
  • 核心技能:godot-master