关于 multiplayer-basics
面向 Godot 4.3+ 开发者的多人游戏基础技能,介绍客户端—服务器架构、RPC、权限管理及对象生成。示例涵盖 GDScript 与 C#,并总结连接、断开和同步方面的常见问题。
主要功能
- 01客户端—服务器架构基础
- 02ENetMultiplayerPeer 连接设置
- 03RPC 模式与传输选项
- 04多人权限与网络对象生成
- 05加入流程及断线处理
适用场景
- 01搭建多人游戏的服务器与客户端
- 02通过 RPC 传递游戏事件和玩家输入
- 03实现玩家加入、生成与断线清理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill multiplayer-basics安装后在 Claude Code 中输入/multiplayer-basics确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/disconnect-handling.mdenet-setup.mdplayer-join-flow.mdspawning-networked-objects.md详细介绍
Godot 4.3+ 中的多人游戏基础
所有示例均适用于 Godot 4.3+,不使用已弃用的 API。先展示 GDScript,之后是 C#。
**相关技能:**有关状态同步和插值,请参阅 multiplayer-sync。有关无头导出和服务器部署,请参阅 dedicated-server。
1. 多人游戏架构
Godot 基于 MultiplayerAPI 使用客户端-服务端模型。一个对等方充当服务端,其余对等方均为客户端。每个对等方都有一个由网络层分配的唯一整数 ID:
| 对等方 ID | 角色 |
|---|---|
1 | 服务端(始终如此) |
2+ | 已连接的客户端——随机生成的唯一 ID,并非连续递增 |
多人游戏权限是指对某个节点的所有权。只有拥有权限的对等方才应读取输入并驱动该节点的状态。默认情况下,服务端(对等方 1)拥有每个节点的权限。调用 set_multiplayer_authority(peer_id) 可将所有权转移给客户端——必须在每个对等方上调用,因为该调用仅在本地生效,不会进行复制。
Server (peer 1)
├── Owns game state by default
├── Spawns and validates objects
└── Routes RPCs
Client (peer 2, 3, …)
├── Sends input to server via RPC
└── Receives state updates from server
2. 设置 ENetMultiplayerPeer
双方都使用相同的三个步骤:创建 ENetMultiplayerPeer,调用 create_server(port, max_clients) 或 create_client(address, port),然后将其赋给 multiplayer.multiplayer_peer,并连接 MultiplayerAPI 信号(peer_connected、peer_disconnected、connected_to_server、connection_failed,以及 server_disconnected——参见 §7)。检查 create_* 的返回值——它会返回一个 Error;否则,静默出现的 ERR_CANT_CREATE(端口已被占用)看起来就和程序卡住完全一样。
服务端的对等方 ID 始终为 1;客户端会收到随机生成的唯一 ID,因此不要假设 ID 会连续递增。
GDScript 和 C# 中包含所有信号处理器的完整服务端和客户端实现:references/enet-setup.md
3. RPC
@rpc(GDScript)/ [Rpc](C#)用于标记可通过网络调用的方法。请谨慎选择模式和传输设置——它们会影响安全性和性能。
RPC 模式
| 模式 | 可以调用的对象 | 执行位置 |
|---|---|---|
"authority"(默认) | 只有拥有权限的对等方 | 被发送到的对等方 |
"any_peer" | 任何已连接的对等方 | 被发送到的对等方 |
传输模式
| 模式 | 传递方式 | 顺序 | 用途 |
|---|---|---|---|
"reliable" | 保证送达 | 按顺序 | 聊天、生成事件、重要状态 |
"unreliable" | 尽力传递 | 无序 | 高频位置更新 |
"unreliable_ordered" | 尽力传递 | 按通道排序 | 平滑的移动数据流 |
GDScript
# chat.gd
extends Node2D
# Any peer can call; server validates then broadcasts to all peers.
@rpc("any_peer", "reliable")
func send_chat_message(text: String) -> void:
if not multiplayer.is_server():
return
var sender_id := multiplayer.get_remote_sender_id()
_broadcast_chat.rpc(sender_id, text)
# Only the authority (server) can call this; runs on every peer.
@rpc("authority", "reliable", "call_local")
func _broadcast_chat(sender_id: int, text: String) -> void:
print("[%d]: %s" % [sender_id, text])
# Client → server: request to spawn an object.
@rpc("any_peer", "reliable")
func request_spawn(scene_path: String, spawn_position: Vector2) -> void:
if not multiplayer.is_server():
return
# Server validates and performs the actual spawn.
var scene: PackedScene = load(scene_path)
if scene == null:
return
var instance := scene.instantiate()
instance.global_position = spawn_position
get_tree().root.add_child(instance)
# High-frequency sync; unreliable_ordered + a channel keeps this off other RPC traffic.
@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
global_position = pos
发送给指定对等方:
# Send to everyone (including self if call_local is set):
send_chat_message.rpc("Hello!")
# Send to one specific peer:
send_chat_message.rpc_id(target_peer_id, "Hello!")
C#
// Chat.cs
using Godot;
public partial class Chat : Node2D
{
// Any peer can call; executes on the server only.
[Rpc(MultiplayerApi.RpcMode.AnyPeer, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
public void SendChatMessage(string text)
{
if (!Multiplayer.IsServer()) return;
int senderId = Multiplayer.GetRemoteSenderId();
Rpc(MethodName.BroadcastChat, senderId, text);
}
// Authority only; runs on every peer including the caller.
[Rpc(MultiplayerApi.RpcMode.Authority,
CallLocal = true,
TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
private void BroadcastChat(int senderId, string text)
=> GD.Print($"[{senderId}]: {text}");
// Client → server: request a spawn.
[Rpc(MultiplayerApi.RpcMode.AnyPeer, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
public void RequestSpawn(string scenePath, Vector2 spawnPosition)
{
if (!Multiplayer.IsServer()) return;
var scene = GD.Load<PackedScene>(scenePath);
if (scene == null) return;
var instance = scene.Instantiate<Node2D>();
instance.GlobalPosition = spawnPosition;
GetTree().Root.AddChild(instance);
}
// High-frequency position sync.
[Rpc(MultiplayerApi.RpcMode.Authority,
CallLocal = true,
TransferMode = MultiplayerPeer.TransferModeEnum.UnreliableOrdered,
TransferChannel = 1)]
public void SyncPosition(Vector2 pos)
=> GlobalPosition = pos;
}
在 C# 中发送给指定对等方:
// Broadcast to all:
Rpc(MethodName.SendChatMessage, "Hello!");
// Send to one peer:
RpcId(targetPeerId, MethodName.SendChatMessage, "Hello!");
4. 权限模型
每个节点恰好有一个拥有权限的对等方——该对等方有权为该节点发送状态更新。其他对等方应将收到的状态视为只读。
GDScript
# player.gd
extends CharacterBody2D
func _ready() -> void:
# multiplayer.get_unique_id() = this peer's ID; server assigns authority during spawn (see Section 6).
pass
func _physics_process(delta: float) -> void:
# Guard: authority-only input and movement.
if not is_multiplayer_authority():
return
var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = direction * 200.0
move_and_slide()
sync_position.rpc(global_position)
@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
if not is_multiplayer_authority():
global_position = pos
func print_authority_info() -> void:
print("My peer ID : %d" % multiplayer.get_unique_id())
print("Authority : %d" % get_multiplayer_authority())
print("Am I auth? : %s" % str(is_multiplayer_authority()))
C#
// Player.cs
using Godot;
public partial class Player : CharacterBody2D
{
public override void _PhysicsProcess(double delta)
{
// Guard: authority-only input.
if (!IsMultiplayerAuthority()) return;
var direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
Velocity = direction * 200f;
MoveAndSlide();
Rpc(MethodName.SyncPosition, GlobalPosition);
}
[Rpc(MultiplayerApi.RpcMode.Authority,
CallLocal = true,
TransferMode = MultiplayerPeer.TransferModeEnum.UnreliableOrdered,
TransferChannel = 1)]
private void SyncPosition(Vector2 pos)
{
if (!IsMultiplayerAuthority())
GlobalPosition = pos;
}
}
API 摘要:
| 方法 | 返回值 | 说明 |
|---|---|---|
multiplayer.get_unique_id() | int | 此对等方的 ID |
get_multiplayer_authority() | int | 拥有此节点的对等方 ID |
is_multiplayer_authority() | bool | 如果此对等方拥有此节点,则为 True |
set_multiplayer_authority(id) | void | 转移所有权;仅在本地生效——必须在每个对等方上调用(例如,在生成函数中) |
5. 生成网络对象
使用 MultiplayerSpawner 在各个对等端之间复制场景实例。注册场景(add_spawnable_scene() 或检查器中的 Auto Spawn List);当服务器将其中一个场景实例添加为 spawn_path 节点的直接子节点时,生成器会在每个对等端上实例化它。对于需要数据(位置、所有者)的生成操作,请设置 spawn_function,并在服务器上调用 spawn(data)。
有关
MultiplayerSpawner场景设置和服务器生成流程(GDScript + C#),请参阅 references/spawning-networked-objects.md。
6. 玩家加入流程
完整的大厅加入生命周期:对等端连接 → 服务器分配位置 → 加载大厅场景 → 生成玩家节点 → 向所有客户端广播对等端列表 → 收到“开始比赛” RPC 后,将所有对等端切换到游戏场景。
有关完整的 GDScript 和 C# 实现(对等端连接处理程序、位置分配、大厅状态、游戏场景切换),请参阅 references/player-join-flow.md。
7. 断开连接处理
监听 multiplayer API 上的 peer_disconnected(id)。在服务器上:释放已断开连接的对等端的玩家节点,并广播更新后的对等端列表。在客户端上:检测服务器断开连接,并跳转到重新连接/主菜单界面。
有关超时检测设置、服务器端清理和客户端重新连接流程(GDScript + C#),请参阅 references/disconnect-handling.md。
8. 常见问题
| 问题 | 症状 | 解决方法 |
|---|---|---|
| 对错误的权限方调用 RPC | 方法从未运行;接收方记录 RPC '…' is not allowed on node … | 发送前检查 is_multiplayer_authority();仅在确有需要时使用 "any_peer" |
| 无序 RPC 导致不同步 | 位置抖动或跳变 | 对数据流使用 "unreliable_ordered";对关键状态变更使用 "reliable" |
在 _process 而非 _physics_process 中读取输入 | 不同帧率下移动不同步 | 始终在 _physics_process 中移动 CharacterBody2D;同步 RPC 也从那里发送 |
处理输入前未检查 is_multiplayer_authority() | 每个对等端都能控制所有玩家 | 在输入处理开头添加 if not is_multiplayer_authority(): return 守卫 |
未使用 MultiplayerSpawner 进行生成 | 对象出现在服务器上,但客户端上缺失 | 将复制的实例添加到 MultiplayerSpawner 的 spawn_path 下(场景需在其可生成列表中),或使用其 spawn() 和 spawn_function |
忘记在权限方 RPC 上设置 call_local | 服务器状态与自身节点不一致 | 当发送方也需要在本地执行 RPC 时,添加 "call_local" |
客户端连接前调用 rpc() | 错误 Trying to call an RPC via a multiplayer peer which is not connected. | 客户端仅在 connected_to_server 后发送 RPC(分配对等端前,默认的 OfflineMultiplayerPeer 只运行 call_local RPC,且仅在本地运行) |
导出版本中未移除 res:// 场景 | 客户端可以读取仅供服务器使用的脚本 | 将仅供服务器使用的文件排除在客户端预设之外(Resources 选项卡:导出模式或排除筛选器),或使用 PCK 加密敏感的服务器代码 |
9. 检查清单
-
ENetMultiplayerPeer.create_server()/create_client()返回OK后,再赋值给multiplayer.multiplayer_peer - 已连接全部五个多人游戏信号:
peer_connected、peer_disconnected、connected_to_server、connection_failed、server_disconnected - 每个读取玩家输入的节点都使用
if not is_multiplayer_authority(): return进行保护 - 输入处理和
sync_positionRPC 都位于_physics_process中,而非_process - 有意选择 RPC 模式:仅客户端 → 服务端调用使用
"any_peer";服务端 → 客户端调用使用"authority" - 仅对高频更新(位置、旋转)使用不可靠 RPC;事件(生成、伤害、聊天)使用可靠 RPC
- 在首位玩家加入前,为
MultiplayerSpawner配置所有可生成场景 - 生成每个玩家节点时,在所有对等端调用
set_multiplayer_authority(peer_id)(例如在spawn_function内) -
peer_disconnected处理程序会释放玩家节点,并将其从跟踪集合中移除 - 客户端上的
server_disconnected处理程序会返回主菜单,并将multiplayer.multiplayer_peer设为 null - 在断开连接回调中解引用任何存储的节点引用前,检查
is_instance_valid() - 在
connected_to_server触发前,不调用客户端rpc()