强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Godot 4 GDScript 教程 / 21 - 多人网络

21 - 多人网络

Godot 4 对网络系统进行了全面重写,采用基于 MultiplayerAPI 的新架构,支持 ENet、WebSocket、WebRTC 等多种传输层。本文将从零开始构建一个可运行的多人联机框架。


核心概念

概念说明
Peer网络连接端(客户端或服务端)
MultiplayerAPI高层网络抽象,管理 RPC 与状态同步
ENetMultiplayer基于 UDP 的可靠传输层,适合局域网/低延迟场景
Authority节点的权威方(默认是服务端)
RPC远程过程调用,在不同端执行函数
MultiplayerSpawner自动同步场景实例化
MultiplayerSynchronizer自动同步节点属性

💡 Godot 4 的网络架构采用 权威服务器 模式:服务端拥有游戏状态的最终决定权,客户端发送输入请求。


ENetMultiplayer 基础

创建服务器

# server.gd
extends Node

const PORT = 9999
const MAX_CLIENTS = 8

var peer: ENetMultiplayerPeer

func _ready():
    start_server()

func start_server():
    peer = ENetMultiplayerPeer.new()
    var error = peer.create_server(PORT, MAX_CLIENTS)
    if error != OK:
        push_error("无法创建服务器: " + error_string(error))
        return

    multiplayer.multiplayer_peer = peer

    # 监听连接事件
    multiplayer.peer_connected.connect(_on_peer_connected)
    multiplayer.peer_disconnected.connect(_on_peer_disconnected)

    print("服务器已启动,端口: ", PORT)

func _on_peer_connected(id: int):
    print("玩家已连接: ", id)
    # 这里可以发送初始游戏状态给新玩家

func _on_peer_disconnected(id: int):
    print("玩家已断开: ", id)
    # 清理该玩家的游戏对象

客户端连接

# client.gd
extends Node

const SERVER_IP = "127.0.0.1"
const PORT = 9999

var peer: ENetMultiplayerPeer

func _ready():
    connect_to_server()

func connect_to_server():
    peer = ENetMultiplayerPeer.new()
    var error = peer.create_client(SERVER_IP, PORT)
    if error != OK:
        push_error("无法连接服务器: " + error_string(error))
        return

    multiplayer.multiplayer_peer = peer

    multiplayer.connected_to_server.connect(_on_connected)
    multiplayer.connection_failed.connect(_on_connection_failed)
    multiplayer.server_disconnected.connect(_on_server_disconnected)

func _on_connected():
    print("已连接到服务器,我的 ID: ", multiplayer.get_unique_id())

func _on_connection_failed():
    print("连接失败")

func _on_server_disconnected():
    print("与服务器断开连接")

⚠️ 注意create_server()create_client() 的返回值必须检查,常见错误是端口被占用。


RPC 新语法(@rpc 注解)

Godot 4 使用 @rpc 注解替代了旧版的 rpc() / rpc_id() 调用方式。

@rpc 注解参数

参数可选值说明
authorityany, server谁可以调用此 RPC
modecall_remote, call_local是否本地也执行
syncsync, no_sync是否同步到新加入的玩家
transfer_modereliable, unreliable, unreliable_ordered传输可靠性

基本用法

# player.gd
extends CharacterBody3D

@export var speed: float = 5.0
var health: int = 100

# 任何端都可以调用,远程执行,可靠传输
@rpc("any_peer", "call_remote", "reliable")
func take_damage(amount: int):
    health -= amount
    print("玩家 ", name, " 受到 ", amount, " 伤害,剩余生命: ", health)

    if health <= 0:
        die()

# 只有服务端可以调用,所有端执行(包括调用者)
@rpc("authority", "call_local", "sync")
func sync_position(pos: Vector3):
    global_position = pos

# 客户端发送输入到服务端
@rpc("any_peer", "call_remote", "reliable")
func request_move(direction: Vector3):
    # 只在服务端执行
    if not is_multiplayer_authority():
        return

    # 服务端验证并执行移动
    velocity = direction * speed
    move_and_slide()

    # 广播新位置给所有客户端
    sync_position.rpc(global_position)

发送 RPC

# 从客户端调用服务端函数
func _physics_process(_delta):
    if not is_multiplayer_authority():
        var direction = Input.get_vector("left", "right", "forward", "back")
        if direction != Vector3.ZERO:
            request_move.rpc_id(1, direction)  # 发送给 ID=1(服务端)

💡 rpc_id(1, ...) 表示只发送给服务端(权威端 ID 始终为 1)。rpc(...) 则广播给所有端。


MultiplayerSpawner —— 场景同步

MultiplayerSpawner 自动处理多人场景中的节点实例化同步。

# main_scene.gd
extends Node3D

@onready var spawner: MultiplayerSpawner = $MultiplayerSpawner
@onready var spawn_point: Node3D = $SpawnPoint

var player_scene = preload("res://player.tscn")

func _ready():
    # 配置 Spawner 的生成路径
    spawner.spawn_path = self.get_path()

    # 添加可生成的场景
    spawner.add_spawnable_scene("res://player.tscn")

    if multiplayer.is_server():
        multiplayer.peer_connected.connect(_add_player)
        multiplayer.peer_disconnected.connect(_remove_player)

        # 为已连接的玩家生成角色
        for id in multiplayer.get_peers():
            _add_player(id)

        # 为自己(服务端玩家)也生成
        _add_player(1)

func _add_player(id: int):
    var player = player_scene.instantiate()
    player.name = str(id)
    player.position = spawn_point.global_position
    add_child(player)

func _remove_player(id: int):
    var player = get_node_or_null(str(id))
    if player:
        player.queue_free()

MultiplayerSynchronizer —— 属性同步

用于自动同步节点属性(位置、旋转、动画状态等)。

# 在编辑器中为 Player 场景添加 MultiplayerSynchronizer 节点
# 或通过代码配置:

# player_setup.gd
extends CharacterBody3D

@onready var synchronizer: MultiplayerSynchronizer = $MultiplayerSynchronizer

func _ready():
    # 配置同步属性
    var config = SceneReplicationConfig.new()

    # 同步位置
    config.add_property(self, "global_position")
    config.property_set_sync_mode(
        config.get_properties().size() - 1,
        SceneReplicationConfig.SYNC_MODE_ALWAYS
    )

    # 同步旋转
    config.add_property(self, "global_rotation")
    config.property_set_sync_mode(
        config.get_properties().size() - 1,
        SceneReplicationConfig.SYNC_MODE_ALWAYS
    )

    # 同步动画状态
    config.add_property(self, "animation_state")
    config.property_set_sync_mode(
        config.get_properties().size() - 1,
        SceneReplicationConfig.SYNC_MODE_ON_CHANGE
    )

    synchronizer.replication_config = config

⚠️ 注意MultiplayerSynchronizer 只同步它所在的节点及其子节点的属性。确保 Authority 设置正确(通常是服务端)。


房间管理系统

# room_manager.gd
extends Node

signal room_created(room_id: String)
signal player_joined(player_id: int, room_id: String)
signal player_left(player_id: int, room_id: String)

class Room:
    var id: String
    var host_id: int
    var players: Array[int] = []
    var max_players: int = 4
    var is_started: bool = false

    func is_full() -> bool:
        return players.size() >= max_players

var rooms: Dictionary = {}  # room_id -> Room

@rpc("any_peer", "call_remote", "reliable")
func create_room(player_id: int, max_players: int = 4) -> String:
    if not multiplayer.is_server():
        return ""

    var room_id = _generate_room_id()
    var room = Room.new()
    room.id = room_id
    room.host_id = player_id
    room.max_players = max_players
    room.players.append(player_id)

    rooms[room_id] = room
    room_created.emit(room_id)
    print("房间已创建: ", room_id, " 主持人: ", player_id)
    return room_id

@rpc("any_peer", "call_remote", "reliable")
func join_room(player_id: int, room_id: String) -> bool:
    if not multiplayer.is_server():
        return false

    if not rooms.has(room_id):
        return false

    var room = rooms[room_id]
    if room.is_full():
        return false

    room.players.append(player_id)
    player_joined.emit(player_id, room_id)
    return true

@rpc("any_peer", "call_remote", "reliable")
func leave_room(player_id: int, room_id: String):
    if not rooms.has(room_id):
        return

    var room = rooms[room_id]
    room.players.erase(player_id)

    if room.players.is_empty():
        rooms.erase(room_id)
    elif room.host_id == player_id:
        room.host_id = room.players[0]

    player_left.emit(player_id, room_id)

func _generate_room_id() -> String:
    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    var id = ""
    for i in 6:
        id += chars[randi() % chars.length()]
    return id

💡 实际项目中,房间管理通常放在专用的 Lobby 服务器上,与游戏服务器分离。


客户端预测与延迟补偿

在快节奏游戏中,客户端预测是降低延迟感的关键技术。

# predicted_player.gd
extends CharacterBody3D

var input_buffer: Array[Dictionary] = []  # {tick, input, position}
var server_state: Dictionary = {}  # 服务端最新状态
var current_tick: int = 0

const BUFFER_SIZE = 60  # 保留 1 秒的历史(60 tick/s)

@rpc("any_peer", "call_remote", "unreliable_ordered")
func send_input(tick: int, input_dir: Vector2):
    if not is_multiplayer_authority():
        return

    # 服务端处理输入并回传结果
    var new_pos = _process_movement(global_position, input_dir)
    sync_state.rpc(tick, new_pos)

@rpc("authority", "call_remote", "unreliable_ordered")
func sync_state(tick: int, server_pos: Vector3):
    # 客户端收到服务端状态
    server_state = {"tick": tick, "position": server_pos}

    # 检查预测是否准确
    var predicted = null
    for entry in input_buffer:
        if entry.tick == tick:
            predicted = entry.position
            break

    if predicted and predicted.distance_to(server_pos) > 0.01:
        # 预测偏差过大,进行修正
        _reconcile(server_pos, tick)

func _physics_process(delta):
    current_tick += 1

    if not is_multiplayer_authority():
        # 客户端预测:立即执行移动
        var input_dir = Input.get_vector("left", "right", "forward", "back")
        var predicted_pos = _process_movement(global_position, input_dir)

        # 缓存输入和预测位置
        input_buffer.append({
            "tick": current_tick,
            "input": input_dir,
            "position": predicted_pos
        })

        if input_buffer.size() > BUFFER_SIZE:
            input_buffer.pop_front()

        # 立即应用预测结果
        global_position = predicted_pos

        # 发送输入给服务端
        send_input.rpc_id(1, current_tick, input_dir)
    else:
        # 服务端直接处理
        pass

func _process_movement(pos: Vector3, input_dir: Vector2) -> Vector3:
    var movement = Vector3(input_dir.x, 0, input_dir.y) * 5.0 * get_physics_process_delta_time()
    return pos + movement

func _reconcile(server_pos: Vector3, from_tick: int):
    global_position = server_pos

    # 从服务端状态之后的所有输入重新模拟
    var new_buffer: Array[Dictionary] = []
    for entry in input_buffer:
        if entry.tick > from_tick:
            var corrected = _process_movement(server_pos, entry.input)
            server_pos = corrected
            entry.position = corrected
            new_buffer.append(entry)

    input_buffer = new_buffer
    global_position = server_pos

⚠️ 注意:客户端预测只对本地玩家有效。其他玩家使用插值平滑显示。


聊天系统实战

# chat_system.gd
extends Control

@onready var chat_log: RichTextLabel = %ChatLog
@onready var chat_input: LineEdit = %ChatInput

const MAX_MESSAGES = 100

@rpc("any_peer", "call_local", "reliable")
func send_message(sender_id: int, message: String):
    # 过滤和验证消息
    message = message.strip_edges()
    if message.is_empty() or message.length() > 200:
        return

    # 转义 HTML 防止注入
    message = message.xml_escape()

    var sender_name = "玩家" + str(sender_id)
    if sender_id == 1:
        sender_name = "[服务器]"

    var color = _get_player_color(sender_id)
    chat_log.append_text("[color=%s]%s[/color]: %s\n" % [color, sender_name, message])

    # 限制消息数量
    while chat_log.get_line_count() > MAX_MESSAGES:
        chat_log.remove_line(0)

func _on_chat_input_text_submitted(text: String):
    if text.is_empty():
        return

    var my_id = multiplayer.get_unique_id()
    send_message.rpc(my_id, text)
    chat_input.clear()

func _get_player_color(id: int) -> String:
    var colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD"]
    return colors[id % colors.length()]

游戏开发场景

多人 FPS 联机对战框架

# fps_game_manager.gd
extends Node

enum GameState { LOBBY, PLAYING, ROUND_END }

var current_state: GameState = GameState.LOBBY
var scores: Dictionary = {}  # player_id -> kills
var round_time: float = 0.0
const ROUND_DURATION = 180.0  # 3 分钟

@rpc("authority", "call_local", "sync")
func start_round():
    current_state = GameState.PLAYING
    round_time = 0.0

    # 重置所有玩家分数
    for id in multiplayer.get_peers():
        scores[id] = 0

    # 传送玩家到出生点
    _respawn_all_players()

@rpc("authority", "call_local", "reliable")
func register_kill(killer_id: int, victim_id: int):
    if scores.has(killer_id):
        scores[killer_id] += 1

    # 延迟后重生受害者
    get_tree().create_timer(3.0).timeout.connect(
        func(): _respawn_player(victim_id)
    )

func _process(delta):
    if current_state == GameState.PLAYING:
        round_time += delta
        if round_time >= ROUND_DURATION:
            _end_round()

func _end_round():
    current_state = GameState.ROUND_END
    # 找出获胜者
    var winner_id = scores.keys().reduce(
        func(a, b): return a if scores[a] > scores[b] else b
    )
    print("回合结束!获胜者: ", winner_id)

网络安全基础

安全措施说明
输入验证服务端验证所有客户端输入
速率限制限制 RPC 调用频率
权限检查使用 @rpc 注解限制调用权限
数据校验验证所有网络数据的范围和类型
# security.gd
extends Node

var rpc_timestamps: Dictionary = {}  # peer_id -> [timestamps]
const MAX_RPC_PER_SECOND = 30

func validate_rpc(peer_id: int) -> bool:
    if not rpc_timestamps.has(peer_id):
        rpc_timestamps[peer_id] = []

    var now = Time.get_ticks_msec()
    var timestamps = rpc_timestamps[peer_id]

    # 清理过期记录
    timestamps = timestamps.filter(func(t): return now - t < 1000)
    rpc_timestamps[peer_id] = timestamps

    if timestamps.size() >= MAX_RPC_PER_SECOND:
        print("玩家 ", peer_id, " RPC 频率超限,可能作弊")
        return false

    timestamps.append(now)
    return true

func validate_position(peer_id: int, pos: Vector3, max_speed: float) -> bool:
    # 检查位置是否在合理范围内
    if pos.length() > 10000:
        return false

    # 检查移动速度是否超限(需要记录上一帧位置)
    # ... 省略具体实现
    return true

⚠️ 注意:永远不要信任客户端数据。所有游戏逻辑的最终决定权应该在服务端。


💡 扩展阅读