mcpField
M
multiplayer-basics

multiplayer-basics

github.com/jame581开发889164 次安装4 天前更新
代码仓库

关于 multiplayer-basics

面向 Godot 4.3+ 开发者的多人游戏基础技能,介绍客户端—服务器架构、RPC、权限管理及对象生成。示例涵盖 GDScript 与 C#,并总结连接、断开和同步方面的常见问题。

主要功能

  1. 01客户端—服务器架构基础
  2. 02ENetMultiplayerPeer 连接设置
  3. 03RPC 模式与传输选项
  4. 04多人权限与网络对象生成
  5. 05加入流程及断线处理

适用场景

  1. 01搭建多人游戏的服务器与客户端
  2. 02通过 RPC 传递游戏事件和玩家输入
  3. 03实现玩家加入、生成与断线清理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jame581/godotprompter --skill multiplayer-basics

安装后在 Claude Code 中输入/multiplayer-basics确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
disconnect-handling.md
enet-setup.md
player-join-flow.md
spawning-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_position RPC 都位于 _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()

评论

支持 Markdown