关于 scenario-godot-multiplayer
面向 Godot 4.7.2 多人游戏网络编程的技能,指导开发者设计权限与复制逻辑,并通过多进程、延迟注入和浏览器客户端测试验证。重点是用实测结果检查安全性、延迟和带宽。
主要功能
- 01按对象设计网络权限
- 02构建场景复制配置
- 03运行多客户端会话测试
- 04注入延迟、抖动与丢包
- 05测量复制带宽
- 06验证 WebSocket 与专用服务器
适用场景
- 01检查客户端 RPC 是否经过服务端校验
- 02测试迟加入和玩家离开后的状态
- 03评估网络延迟对移动的影响
- 04估算多人游戏的复制带宽
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ scenario-labs/ skills --skill scenario-godot-multiplayer安装后在 Claude Code 中输入/scenario-godot-multiplayer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/critique.mdexpert-notes.mdgui-paths.mdprocedures.mdsources.mdscripts/gd_net.pyscripts/agentkit/multiplayer/net_audit.gdnet_boot.gdnet_build.gdnet_input.gdnet_lab.gdnet_player.gdnet_prop.gdnet_session_job.gdnet_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/。
立场(专家补充)
- 编写代码前,先确定每个对象由谁负责。 每个对象的数据都必须有负责人(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 后会卡住。
- 在
_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。 - 每个
any_peerRPC 都是一个请求。 首先读取multiplayer.get_remote_sender_id()(测得,在await后它会变成 0),检查发送方是否拥有目标对象,以及传入值是否合理,然后再执行操作。实时测试中,伪造的传送请求(目标为其他玩家)和跳跃 30 m 都遭到拒绝。 - 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)。 - 在有延迟且有三名玩家的情况下测试。 本地主机环境会掩盖所有延迟问题;两名玩家也无法体现旁观者看到的情况(Galffy,iP_xdJ0peFo [00:09:58, 00:42:31])。
gd_net.UdpLagProxy无需 root 权限即可在本地主机上增加延迟、抖动和丢包。 - 将复制成本纳入设计考量。 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])。 - 了解各连接事件会在哪里触发。 客户端加入时,
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)。 - 服务端能阻止速度作弊,但无法阻止透视作弊。 服务端权威模式能阻止不可能的移动和无限生命值;但无法阻止透视或自瞄,因为每个客户端仍会收到所有人的位置。兴趣管理(synchronizer 可见性、服务端视线检查)可限制信息泄露(MfjEB1cowsE [00:18:55];iP_xdJ0peFo [00:53:28])。
- 专用服务端是以“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://)。
工作流
- 网络设计说明。 对每个对象,明确:所有者、要复制的内容(同步器属性和模式)、
哪些内容属于事件(RPC 的模式、传输方式和通道),以及客户端可以请求什么。连续的
unreliable_ordered 更新使用独立的传输通道,通道参数放在
@rpc的最后(文档差异 4、 5);低于速度阈值的闲置玩家不发送任何内容(pA9RGn87Uag [00:42:16])。门槛:每个 对象都必须有所有者;除非需求明确接受作弊,否则生命值、分数或物品栏不得由客户端拥有。 - 用代码构建复制。
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名称,间隔与模式相符)。 - 在 localhost 上测试会话。
gd_net.session(P, roles)在各自独立的 APFS 克隆中运行一个 服务器和 2 到 3 个客户端,并先占用所有机器槽位。覆盖大厅门槛、迟到加入的玩家、离开的 玩家、作弊 RPC,以及通过auth_callback和两端的complete_auth设置的密码(P5)。门槛: 每个客户端都能看到所有玩家;迟到加入的玩家能看到所有玩家;离开玩家的节点在所有位置都被释放; 权限报告正确;伪造的 RPC 被拒绝;日志干净。 - 延迟测试。 通过
UdpLagProxy分别在 100 和 250 ms 下重新运行,然后在 250 ms 延迟、 30 ms 抖动和 5 % 丢包的情况下运行;replication_delay()用于测量远程客户端显示的玩家状态 落后服务器实际状态的程度。门槛:数值符合游戏类型的要求,否则就要规划预测方案 (netfox 或自建方案)。 - 带宽测试。 根据 ENet 计数器获取每个客户端的服务器 KiB/s(结果中的
bandwidth), 按模式、间隔和可见性统计。门槛:最大玩家人数下不超出预算,并限制 fps。 - Web 和传输方式。 使用
transport="websocket"运行相同的会话;对于浏览器构建, 导出 Web,并在无头 Chrome 中运行客户端(流程 P9)。WebSocket 基于 TCP:标记为 “不可靠”的 RPC 仍会以可靠、有序的方式送达,并在丢包时出现队头阻塞;WebRTC 为浏览器提供 不可靠通道,但桌面端需要信令、STUN/TURN 和 webrtc-native(WebRTC 文档; wgIqB6JNcro [00:19:35])。HTTPS 页面需要wss://。后台标签页会停止_process,导致 套接字断开:需要构建重连机制(Web 文档)。门槛:浏览器客户端成功加入并看到所有玩家,且已测量 RTT。 - 专用服务器。 使用
gd_net.server_preset(),导出并比较客户端和服务器包 (pack_report),在不带--headless的情况下运行导出的二进制文件,并连接客户端。设置application/run/flush_stdout_on_print=true,以便写入 systemd 或 journald 日志(文档差异 12; 默认值为 false,已测量)。门槛:dedicated_server功能为 true,显示模式为headless, 包中没有客户端纹理,退出码为 0。 - 查看效果。 使用窗口化观察客户端捕获复制后的世界(
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 m | UdpLagProxy,每个方向增加约 6 ms |
| 50 个道具,ALWAYS,60 fps | 72.6 KiB/s,移动或静止 | 1 个客户端 |
| ON_CHANGE 移动 / 静止 | 98 to 101 / 2.3 KiB/s | 同上 |
ALWAYS,replication_interval 为 0.05 | 21 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_limit | 0 = 不限(默认);限制为 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);无害,已按完全匹配的文本列入白名单。
参考资料
references/procedures.md:P1 至 P14,包含代码、测试 ID 和结果。references/expert-notes.md:专家观点(含时间戳)及已验证的更正。references/critique.md、references/gui-paths.md(Replication 面板、Run Instances、Network Profiler)、references/sources.md。scripts/gd_net.py、scripts/agentkit/multiplayer/:测试框架和套件。