mcpField
S
scenario-godot-multiplayer

scenario-godot-multiplayer

github.com/scenario-labs开发1.2k154 次安装1 天前更新
代码仓库

关于 scenario-godot-multiplayer

面向 Godot 4.7.2 多人游戏网络编程的技能,指导开发者设计权限与复制逻辑,并通过多进程、延迟注入和浏览器客户端测试验证。重点是用实测结果检查安全性、延迟和带宽。

主要功能

  1. 01按对象设计网络权限
  2. 02构建场景复制配置
  3. 03运行多客户端会话测试
  4. 04注入延迟、抖动与丢包
  5. 05测量复制带宽
  6. 06验证 WebSocket 与专用服务器

适用场景

  1. 01检查客户端 RPC 是否经过服务端校验
  2. 02测试迟加入和玩家离开后的状态
  3. 03评估网络延迟对移动的影响
  4. 04估算多人游戏的复制带宽

安装

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

$npx skills add https://github.com/scenario-labs/skills --skill scenario-godot-multiplayer

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

包含的文件

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

references/
critique.md
expert-notes.md
gui-paths.md
procedures.md
sources.md
scripts/
gd_net.py
scripts/agentkit/multiplayer/
net_audit.gd
net_boot.gd
net_build.gd
net_input.gd
net_lab.gd
net_player.gd
net_prop.gd
net_session_job.gd
net_util.gd

详细介绍

Godot 多人游戏(网络程序员)

目标版本:Godot 4.7.2,通过 ENet 和 WebSocket 使用 SceneMultiplayer。

这里的专家级标准是:在有人开始玩之前,已通过多个进程证明联网游戏可正常运行,并注入延迟进行测试:每个对象的权限都已明确记录,每条客户端消息都是经过服务器检查的请求,并且复制开销以 KiB/s 计量。此技能会用代码构建复制,在 localhost 上以无头模式运行一个服务器和 2 至 3 个客户端(另加一个浏览器客户端),并且只有运行数据达标才算通过。如果你可用的技能中缺少此处提到的同级技能,请让用户安装它(npx skills add scenario-labs/skills --skill <name>);在无人值守的情况下,请根据工具架构继续操作,并标明这一缺失。

必备背景: scenario-godot-expert(通道、gd_run、审查循环、4.7 陷阱)。

状态(2026-10-02): 16 项实时测试(最多 3 个客户端、ENet、WebSocket、导出的服务器、无头 Chrome)和 18 项离线测试均已通过。证据:tests/live_evidence/godot-multiplayer/。

立场(专家补充)

  1. 编写代码前,先确定每个对象由谁负责。 每个对象的数据都必须有负责人(AndrooDev,jff9oxO8v1s [00:04:01])。休闲合作游戏中,让客户端负责主体没问题;竞技类或容易作弊的游戏则应由服务端负责主体,只让客户端提供输入(Tamas Galffy,iP_xdJ0peFo [00:03:46, 00:06:47];BatteryAcidDev,GqHTNmRspjU [00:00:33])。 判断标准:作弊的代价与输入延迟的代价之间如何权衡。在 250 ms RTT 下测得:服务端权威模式显示自己的移动需 294 到 310 ms,客户端权威模式需 11 到 22 ms;而客户端权威模式下作弊者跳跃 50 m 后会卡住。
  2. 在 _enter_tree 中根据节点名称设置权威归属。 将每个玩家节点命名为其 peer id;_enter_tree 调用 set_multiplayer_authority(name.to_int()),确保在 _ready 前设置好(Travis Hunter,tK2ACXUGcrY [00:46:16])。不要在那里读取通过 spawn 同步的属性:在测得的每个客户端副本中,_enter_tree 执行时 peer_id 仍为 0。
  3. 每个 any_peer RPC 都是一个请求。 首先读取 multiplayer.get_remote_sender_id()(测得,在 await 后它会变成 0),检查发送方是否拥有目标对象,以及传入值是否合理,然后再执行操作。实时测试中,伪造的传送请求(目标为其他玩家)和跳跃 30 m 都遭到拒绝。
  4. RPC 校验和只比较方法名称,与文档说法不符。 高级文档称校验和涵盖每个 @rpc 声明并忽略参数;但在 4.7.2 中的两次实时运行(M2,第 1 轮和第 2 轮)结果不一致。一侧缺少某个名称时,双方都会记录 "The rpc node checksum failed",RPC 会按排序后的索引匹配:客户端调用了 b,服务端却运行了 a。模式不同只会在每次调用时失败("not allowed");参数数量不同会在每次调用时失败("expected 2 argument(s)");call_local 或传输模式不同则会静默通过。确保服务端和客户端在每个节点路径上使用同一个脚本(单一代码库:Davide Di Staso,nyKBuM9Y_-Q)。
  5. 在有延迟且有三名玩家的情况下测试。 本地主机环境会掩盖所有延迟问题;两名玩家也无法体现旁观者看到的情况(Galffy,iP_xdJ0peFo [00:09:58, 00:42:31])。gd_net.UdpLagProxy 无需 root 权限即可在本地主机上增加延迟、抖动和丢包。
  6. 将复制成本纳入设计考量。 ALWAYS 无论值是否变化,都会在每个网络帧发送数据;值不断变化时,ON_CHANGE 虽可靠,但开销高于 ALWAYS(实测:50 个移动道具时为 72.6,相比 100 KiB/s)。无头服务端不受帧率限制:此处为 145 fps,带宽是 60 fps 的 2.2 到 2.4 倍。可通过 --max-fps、Engine.max_fps 限制帧率,或设置 replication_interval。按可见性裁剪数据(Di Staso [00:14:29])。
  7. 了解各连接事件会在哪里触发。 客户端加入时,peer_connected 会为每个现有 peer(包括服务端 id 1)触发一次;connected_to_server 只会在客户端触发,因此应在此处发送客户端信息,而不是在 peer_connected 中发送(文档差异 7;FinePointCGI e0JLO_5UgQo [00:22:50])。服务端退出后,get_unique_id() 会返回 0:在注册表和生成循环中跳过 id 0(pA9RGn87Uag [00:47:31])。以上均已实测(M14)。
  8. 服务端能阻止速度作弊,但无法阻止透视作弊。 服务端权威模式能阻止不可能的移动和无限生命值;但无法阻止透视或自瞄,因为每个客户端仍会收到所有人的位置。兴趣管理(synchronizer 可见性、服务端视线检查)可限制信息泄露(MfjEB1cowsE [00:18:55];iP_xdJ0peFo [00:53:28])。
  9. 专用服务端是以“dedicated server”模式导出的同一项目。 该模式会启用 dedicated_server 功能,即使没有 --headless,也会强制使用无头显示和虚拟音频输出(在导出的 macOS 二进制文件上实测);而 Strip Visuals 从服务端包中移除了 3.1 MB 的纹理(BatteryAcidDev,jgJuX04cq7k [00:04:47])。

首先确定

游戏类型和玩家人数;谁可能作弊(竞技、合作、仅限好友);平台(浏览器意味着使用 WebSocket 或 WebRTC,不用 ENet);拓扑(专用服务器、监听服务器、中继或 P2P);已选定的后端 (Steam、Nakama、Colyseus、自有服务器);托管预算(云服务需要花钱:部署前先询问);Tick 频率和 目标 RTT(默认服务器为 60 Hz,在 100 和 250 ms 下进行游戏测试)。 默认设置:一个项目;对于竞技内容,采用服务器权威,客户端仅发送输入;桌面端和移动端使用 ENet, Web 端在 TLS 后使用 WebSocket(wss://)。

工作流

  1. 网络设计说明。 对每个对象,明确:所有者、要复制的内容(同步器属性和模式)、 哪些内容属于事件(RPC 的模式、传输方式和通道),以及客户端可以请求什么。连续的 unreliable_ordered 更新使用独立的传输通道,通道参数放在 @rpc 的最后(文档差异 4、 5);低于速度阈值的闲置玩家不发送任何内容(pA9RGn87Uag [00:42:16])。门槛:每个 对象都必须有所有者;除非需求明确接受作弊,否则生命值、分数或物品栏不得由客户端拥有。
  2. 用代码构建复制。 gd_net.install_netkit(P),然后由 net_build.gd(make_sync、make_spawner) 在代码中设置 SceneReplicationConfig 并写入场景;Replication 面板供人操作 (references/gui-paths.md)。玩家:名称 = peer id, add_child(node, true),在 _enter_tree 中设置权限。门槛:net_audit.gd:scene 无任何标记 (根路径可解析,不含 Object 或 RID 属性,不含 @Node@2 名称,间隔与模式相符)。
  3. 在 localhost 上测试会话。 gd_net.session(P, roles) 在各自独立的 APFS 克隆中运行一个 服务器和 2 到 3 个客户端,并先占用所有机器槽位。覆盖大厅门槛、迟到加入的玩家、离开的 玩家、作弊 RPC,以及通过 auth_callback 和两端的 complete_auth 设置的密码(P5)。门槛: 每个客户端都能看到所有玩家;迟到加入的玩家能看到所有玩家;离开玩家的节点在所有位置都被释放; 权限报告正确;伪造的 RPC 被拒绝;日志干净。
  4. 延迟测试。 通过 UdpLagProxy 分别在 100 和 250 ms 下重新运行,然后在 250 ms 延迟、 30 ms 抖动和 5 % 丢包的情况下运行;replication_delay() 用于测量远程客户端显示的玩家状态 落后服务器实际状态的程度。门槛:数值符合游戏类型的要求,否则就要规划预测方案 (netfox 或自建方案)。
  5. 带宽测试。 根据 ENet 计数器获取每个客户端的服务器 KiB/s(结果中的 bandwidth), 按模式、间隔和可见性统计。门槛:最大玩家人数下不超出预算,并限制 fps。
  6. Web 和传输方式。 使用 transport="websocket" 运行相同的会话;对于浏览器构建, 导出 Web,并在无头 Chrome 中运行客户端(流程 P9)。WebSocket 基于 TCP:标记为 “不可靠”的 RPC 仍会以可靠、有序的方式送达,并在丢包时出现队头阻塞;WebRTC 为浏览器提供 不可靠通道,但桌面端需要信令、STUN/TURN 和 webrtc-native(WebRTC 文档; wgIqB6JNcro [00:19:35])。HTTPS 页面需要 wss://。后台标签页会停止 _process,导致 套接字断开:需要构建重连机制(Web 文档)。门槛:浏览器客户端成功加入并看到所有玩家,且已测量 RTT。
  7. 专用服务器。 使用 gd_net.server_preset(),导出并比较客户端和服务器包 (pack_report),在不带 --headless 的情况下运行导出的二进制文件,并连接客户端。设置 application/run/flush_stdout_on_print=true,以便写入 systemd 或 journald 日志(文档差异 12; 默认值为 false,已测量)。门槛:dedicated_server 功能为 true,显示模式为 headless, 包中没有客户端纹理,退出码为 0。
  8. 查看效果。 使用窗口化观察客户端捕获复制后的世界(capture_at);打开捕获结果。门槛: 玩家位置与服务器所示一致;颜色按 peer 区分;没有内容缺失。

一次调用运行测试框架

import sys; sys.path[:0] = ["<skills>/scenario-godot-expert/scripts", "<skills>/scenario-godot-multiplayer/scripts"]
import gd_net
gd_net.install_netkit(P); gd_net.build_scenes(P)          # res://net/world.tscn, player.tscn, props
roles = [gd_net.role("server", role="server", port=24010, lobby_size=2),
         gd_net.role("c1", role="client", port=24011, index=0, delay_s=0.3, cheat_at=2.0),
         gd_net.role("c2", role="client", port=24011, index=1, delay_s=0.3)]
with gd_net.UdpLagProxy(24011, 24010, rtt_ms=100, jitter_ms=0, loss=0.0):
    s = gd_net.session(P, roles)                            # one APFS clone and one Godot per role
s["roles"]["c1"]["result"]["rtt_median_ms"]                 # every NetLab number is in AGENT_RESULT

你自己的游戏使用相同模式:每种角色对应一个作业脚本(role(name, script=...)),用户参数放在 -- 之后,每个进程都会打印 AGENT_RESULT。每个项目使用唯一端口;两个角色不得共用项目文件夹。

预测、回滚与后端(此处未运行)

netfox 提供服务器权威模式,并支持客户端预测和回滚:在网络帧收集输入, 在 _rollback_tick 中运行逻辑,在 move_and_slide 周围使用 NetworkTime.physics_factor, 并使用 TickInterpolator 进行显示(GqHTNmRspjU [00:06:58, 00:18:55, 00:21:24];iP_xdJ0peFo [00:28:00])。 先用一个小型原型验证:它有自己的主张(GqHTNmRspjU [00:34:53])。仅基于输入的确定性回滚适用于格斗游戏和类似 RTS 的游戏,玩家数为 2 到 8 人(David Snopek, zvqQPbT8rAE)。Steam(GodotSteam、SteamMultiplayerPeer)、Nakama、Colyseus 和 WebRTC P2P 只需替换对等端和大厅调用;权威、生成器和同步器代码保持不变(Gwizz,fUBdnocrc3Y; AndrooDev,wgIqB6JNcro [00:07:18])。这些方案都未在此处运行:它们需要第三方插件、账号或信令服务器。引用 API 前,请先对照已安装的版本进行验证。

数值

值此处测得(2026-10-02,M 系列 Mac,本地主机)对比
RPC 往返,无代理16 to 19 ms两端均为 60 fps;每个方向一帧是主要耗时
ENet RTT 统计15 to 23 ms同上
自身输入到可见动作,服务器权威47 to 52 ms约 3 帧
远程视图落后于服务器实际状态20 to 30 ms,速度为 3 m/s 时误差 0.07 m无代理
RTT 为 100 / 250 ms 时的相同情况70 to 80 / 150 to 155 ms;0.22 / 0.44 mUdpLagProxy,每个方向增加约 6 ms
50 个道具,ALWAYS,60 fps72.6 KiB/s,移动或静止1 个客户端
ON_CHANGE 移动 / 静止98 to 101 / 2.3 KiB/s同上
ALWAYS,replication_interval 为 0.0521 to 22 KiB/s同上
ALWAYS,未限速的无头服务器(145 fps)160 to 175 KiB/s同上
auth_timeout 默认值;踢出无响应对等端3.0 s;设置为 1.5 后为 1.50 s服务器
Web 导出,单线程40 MB,wasm 39.5 MB(10.1 MB gzip)移交给 scenario-godot-performance-export
专用服务器包 / 客户端包0.11 / 3.27 MB(已剥离 3.1 MB 纹理)同一项目
spawn_limit0 = 不限(默认);限制为 2:第三次 spawn() 会记录 "Spawn limit reached!",返回 null,客户端获得 2 个M14;将其提高到最大玩家数(jgJuX04cq7k [00:18:34])

质量门槛

  • 可测量:net_audit 无标记;每个角色均为 ok,且无意外引擎错误; 所有位置上的 players_seen_max 均等于玩家数;权威主体为 1,输入 = 对等端;传送 计数符合预期;RTT 和 replication_delay 在 100 和 250 ms 下的表现;各模式下的 KiB/s;已 审查 rpc_audit,rpc_pair_check 为 ok;导出的服务器启动字典正确。
  • 视觉:观察者截图显示每个玩家;在 250 ms 下,传送后无拖影;浏览器 截图显示已复制的玩家。

常见错误

错误表现修复
仅在服务器上生成玩家,且没有生成器,或生成器的 spawn_path 为空客户端看不到任何人使用带有 spawn_path 且列表中包含该场景的 MultiplayerSpawner;服务器调用 add_child
在 _enter_tree 中从同步属性读取权限客户端上的权限始终为 1从 name.to_int() 获取权限
运行时节点未使用 add_child(node, true)出现 @Node@2 名称,RPC 报错“找不到节点”使用易读的名称
在调用 get_remote_sender_id() 前使用 await发送方为 0,验证错误地通过或失败先读取发送方
仅在服务器脚本中添加 RPC校验和错误,运行了错误的方法使用同一个脚本,或使用 rpc_pair_check
将对象或资源作为 RPC 参数没有错误;接收方收到 EncodedObjectAsID发送 ID、路径或纯数据
在竞技游戏中由客户端拥有其角色主体速度和传送作弊会生效仅由客户端控制输入,并由服务器模拟
无头服务器未设帧率上限带宽变为 2.4 倍使用 --max-fps 60 或设置间隔
为 P2P 风格功能关闭 server_relay报错“尝试使用未知对等端 ID 调用 RPC”保持中继开启,或通过服务器路由
以错误顺序释放联网分支报错“当前未激活”“找不到节点”离线对等端、释放内容、移除自定义 API,再释放分支
使用普通模式导出服务器生成完整大小的包,并打开窗口使用专用服务器导出模式,并选择 Strip Visuals
在 assert() 中启动服务器在编辑器中运行正常;发布导出版本中服务器从未开始监听使用 var err := peer.create_server(port),然后检查 err(M15:发布版本会移除 assert 函数体)
从 peer_connected 发送客户端信息,或注册 ID 0后加入的客户端出现重复节点,或出现一个虚假玩家使用 connected_to_server;跳过 ID 0
客户端和服务器使用不同的 Godot 版本连接或协议失败使用相同的 4.7.2 构建版本(协议在 4.3 中发生了变化)

交接

  • 接收:来自 scenario-godot-gameplay 和 scenario-godot-3d-world 或 scenario-godot-2d 的游戏场景和玩家控制器;来自 scenario-godot-architecture 的架构规则。
  • 交付:网络设计说明、套件场景或脚本、会话证据 JSON、服务器预设;Web 构建大小和请求标头交给 scenario-godot-performance-export,CI 作业交给 scenario-godot-pipeline-automation,大厅界面交给 scenario-godot-ui。

Godot 4.7 说明

  • get_rpc_config 已更名为 get_node_rpc_config(4.5);@rpc 表位于 Script.get_rpc_config()。
  • 属性名是 auth_timeout;文档页面误写为 auth_timout。
  • 4.7.2 修复了删除由 spawner 生成的节点后停止复制的对等端问题。
  • Godot 3 中的名称(NetworkedMultiplayerENet、remote、rset、get_rpc_sender_id)均不存在;gd_net.rpc_audit 会标记它们。
  • 桌面端的 WebRTC 需要 webrtc-native 扩展:若没有该扩展,initialize() 会返回 OK,但只记录错误日志(实测)。
  • 未解决:机器负载较高时,20 次 ALWAYS 运行中有 4 次只发送了约 10 个数据包/秒,而不是 68 个(ENet throttle 为 0)。在长时间会话中记录 NetUtil.enet_throttle()(P8)。
  • 两个对等端在同一服务器帧内离开时,会记录“Unable to send packet on channel 0”(ENet)或 ready_state != STATE_OPEN(WebSocket);无害,已按完全匹配的文本列入白名单。

参考资料

评论

支持 Markdown