Files
agame/.workbuddy/memory/2026-09-22.md
T
2026-09-22 17:51:52 +08:00

11 KiB
Raw Blame History

2026-09-22

给 Godot 项目 agame 接上局域网联机(ENet 基础版)

全流程通过 godot-ai MCP 操作编辑器完成,E:\GD_Projects\agame 项目。

落地结果

  • 新增 autoload/net.gd(Autoload 名 Net):传输层 + 开局/加入/断开 + 玩家生成回收 + 主菜单/HUD(代码建 UI,无 .tscn)。
  • 新增 world/main.gd:把 player.tscn 注册给 MultiplayerSpawner.spawn_function,spawn_function 里 set_multiplayer_authority(peer_id, true) + add_to_group("net_player")。
  • 新增 player/player_sync.tres(SceneReplicationConfig),同步字段:.:position、Body:rotation,replication_mode = 1(ALWAYS)。
  • player/player.tscn:根下加 Sync(MultiplayerSynchronizer,root_path 保持默认 ..),绑定上面那个 .tres。
  • world/main.tscn:删掉硬编码的 Player 实例,改挂 MultiplayerSpawner(spawn_path = ..)。
  • player/player.gd:加 _is_local;远端玩家 camera_rig.queue_free() + 关物理 + 关 unhandled input。
  • 新增 tests/test_net_replication.gd(McpTestSuite),3 个用例 12 条断言全绿,钉住同步配置不被静默写坏。

关键结论 / 踩坑

  • 节点 authority 不继承:必须 set_multiplayer_authority(id, true),否则子节点 MultiplayerSynchronizer 仍是服务器 authority,同步不发。
  • 远端玩家必须拆相机并关物理,否则多台 Camera3D 抢渲染、双重模拟导致抖动。
  • 去中心化 authority 模型跑通(host=1 本地操控 / client 远端回放,client 侧镜像)+ ENet 中继正常。
  • 命令行调试入口:Net._apply_cmdline() 支持 -- --host / -- --join <ip>。
  • 项目已存在的无关报错:res://addons/ziva_agent/ziva_agent.gdextension 文件缺失,每次启动都刷 3 行 ERROR。 已修复: 插件目录已删除,但 .godot/extension_list.cfg 里残留了一行 res://addons/ziva_agent/ziva_agent.gdextension。 filesystem_manage(op="scan") 不会清掉它(Godot 只在运行中观测到 .gdextension 增删时才重写该文件), 直接删掉那一行即可。已用 headless 启动验证:3 行 ERROR 消失。
  • AppID 仍是 480(Spacewar 测试,Steam 客户端未装 → status:1),换 Steam P2P 时只需改 Net.host_game/join_game。

输出文档

  • 新增 docs/联机框架使用指南.md:面向零联机基础的人,含概念(peer/authority/复制 vs RPC)、文件地图、 三种跑法、四个「新功能怎么同步」配方、Steam P2P 迁移(直连 SteamID 版 + 大厅版)、调试与已知限制。
  • Steam API 签名已用探针脚本让 GDScript 分析器逐个校验(本项目装的是 camelCase 风格 GodotSteam):
    • Steam.createLobby(type, max_members) 返回 void,结果走 lobby_created(connect_result, lobby_id) 信号
    • Steam.joinLobby(lobby_id) → lobby_joined(lobby_id, permissions, locked, response) 信号
    • Steam.requestLobbyList() → lobby_match_list(lobbies: Array) 信号;成功码 = 1
    • Steam.getSteamID();setLobbyData/getLobbyData/getLobbyOwner/getNumLobbyMembers/getLobbyMemberByIndex
    • SteamMultiplayerPeer:create_host(port=0)、create_client(steam_id, port=0)、host_with_lobby(lobby_id)、 connect_to_lobby(lobby_id)、add_peer(steam_id, port=0)、set_no_delay/set_no_nagle/set_debug_level
  • 校验手法(可复用):写临时 .gd 调用候选 API,script_create 返回的 diagnostics 会指出「方法不存在」「返回 void 不能赋值」等,比查文档更快更准。
  • 新增 docs/联机实战教程-由浅入深.md:九关递进教程(血量同步 → 颜色认领 → 头顶名牌 → 挥手 RPC → 两段式攻击 → 子弹 Spawner → 服务器权威血量 → 远端插值 → 玩家列表)。按键约定 H/G/K/J。
  • 教程里引入的两个工程改动(后续实现时要跟着做):player.gd 加 class_name PlayerController、 main.gd 加 class_name GameWorld、bullet.gd 加 class_name Bullet、bullets.gd 加 class_name BulletPool, 这样跨脚本调用才有编译期检查。
  • 写教程时抓到一个真 bug:parent.broadcast_hit(...) 是本地调用,伤害传不到对方机器,必须 world.broadcast_hit.rpc(...)。GDScript 对漏 .rpc() 不报任何错,这是最阴的一类联机 bug。
  • 第二个发现:@rpc("authority") 的检查是「发起方 peer id == 被调节点 authority」,所以服务器不能 调用别人玩家节点上的 authority 模式 RPC。广播函数必须放在 authority 为 1 的节点上(世界根节点 Main 天然满足)。
  • 教程全部代码用「拼成一个文件做整体编译」的方式验证过:diagnostics: [] 零错误。 GDScript 对动态成员访问极其宽容(Node 上调脚本方法、Node3D.new() 设脚本属性都不报错), 所以「编译通过」不等于「语义正确」,逻辑对错还得靠人读。

UI 改为节点配置(用户要求:不要代码生成 UI)

  • 新增 autoload/net.tscn(根 Net 是 CanvasLayer,layer=20): Menu(CenterContainer→PanelContainer→VBox: Title/AddressEdit/HostButton/JoinButton/StatusLabel/HintLabel)
    • Hud(Control→VBox: StatusLabel/HintLabel)。两个按钮的 pressed 在 .tscn 里连接到 Net。
  • autoload/net.gd 重写:extends CanvasLayer,删掉全部 _build_*,改为 @onready 节点引用 + 状态更新 (_on_host_button_pressed / _on_join_button_pressed 由场景信号触发)。
  • project.godot 的 autoload 改成指向场景:Net="*res://autoload/net.tscn"(原来指向 net.gd)。 这样 Net 这个全局名就是那个 CanvasLayer 根节点,/root/Net 路径不变,main.gd/player.gd 无需改动。
  • 建场景的做法:scene_manage(create) → batch_execute(13 个 create_node) → batch_execute(29 个 set_property,undo=false) → signal_manage(connect) ×2 → script_attach → scene_save。Godot 自己序列化,比手写 .tscn 可靠。
  • 验证:双实例 headless 联调通过(主机/客户端各自一个「本地操控」,日志镜像);编辑器内运行零报错。
  • 文档同步:联机框架使用指南.md 文件地图;联机实战教程-由浅入深.md 关卡 3 / 关卡 9 改成「在编辑器加节点」写法。

玩家的 UI 也改成节点配置(同一轮完成)

  • player.tscn 新增 Hud (CanvasLayer) → HealthLabel (Label)(位置 16,56)。
  • player.gd:删掉 _make_health_label()(原来用代码建 CanvasLayer + Label), 改为 @onready var _hud: CanvasLayer = $Hud / @onready var _health_label: Label = $Hud/HealthLabel; 远端分支加 _hud.visible = false(否则 N 份血条叠在同一屏幕坐标,显示的是别人的血量); 本地分支末尾补 _refresh_health_label()(@onready 赋值晚于同步写入,那次 setter 会被 null 判断跳过)。
  • _report_identity() 详细自检已恢复。
  • 确认 player_sync.tres 现在有 3 个字段:.:position、Body:rotation、.:health(用户自己加的,正确)。
  • 文档同步:联机实战教程-由浅入深.md 关卡 1 改成「先在 player.tscn 里加 Hud/HealthLabel 节点」; 联机框架使用指南.md 的 player.tscn 结构图。

血条改到玩家头顶(Label3D)

  • player.tscn:删掉 Hud (CanvasLayer),改为 HealthLabel (Label3D) 直接挂在 Player 下, position (0, 2.05, 0)、billboard=1、no_depth_test=true、font_size=64、pixel_size=0.005(默认值)、outline_size=8。
  • player.gd:@onready var _health_label: Label3D = $HealthLabel,删掉 _hud 引用与 _hud.visible = false。
  • 顺带消掉一个坑:用 Label3D 后,血条天然待在各自头顶,不再需要「远端玩家隐藏血条」那一步 (屏幕坐标的 CanvasLayer 才需要,否则 N 份叠在同一屏幕位置)。 现在默认人人可见别人的血量 —— 因为 health 是同步字段。
  • 尺寸换算:Label3D 的文字世界高度 ≈ font_size × pixel_size。64 × 0.005 ≈ 0.32 米。
  • 若要与关卡 3 的头顶名牌共存:血量放 y=2.05,名牌放 y=2.3,不重叠。
  • 验证:双实例 headless 联调,两个日志里 ERROR|Invalid|null 一条都没有。

反复出现的坑:编辑器把外部写盘的内容覆盖回去

net.gd 用 MCP script_create 写成功后,磁盘上又变回了旧版(extends Node + _build_ui 全在)。 原因:编辑器的脚本编辑器标签页里缓存着旧文本,资源 reload ≠ 标签页文本刷新, 之后一次保存就把旧文本写回去了。而且工具返回的 reloaded: true 会让人误以为已经生效。

规避办法:

  1. 每次写完立刻从磁盘回读校验(Grep/Read 关键行),别信工具的 reloaded。
  2. 改 Godot 项目里的脚本/场景一律走 godot-ai MCP,不要用外部写盘。
  3. 交付时提醒用户:如果编辑器标签页里还是旧内容,关掉标签页重开(或重启编辑器), 并且在确认内容更新之前不要按 Ctrl+S。

net.gd 一共被还原了两次(第二次在 17:41~17:43 之间,导致 git status 里 autoload/net.gd 一度没有差异)。第二次修复后连续观察 30 秒无变化才确认落地。 排查时用:读文件首行 + 查 _build_ui + 看 size/mtime,隔一段时间对比。

仓库状态(供用户查证)

  • .gitignore 只忽略 .godot/ 和 /android/,业务文件不会被忽略。
  • 最后一个提交是 2ecab99(17:32「扣血同步」,用户的关卡 1);此后所有改动都还没提交, 且 origin/master...master = 0 0,即一个都没推送。
  • 未提交清单:autoload/net.gd(-121 行)、player/player.gd、player/player.tscn、 project.godot、两份 docs、.workbuddy/memory/...;autoload/net.tscn 是 untracked(新文件)。
  • 用户说「git 上看不到差异」,很可能是因为:① 看的是远端网页(没推送); ② GUI 客户端默认不显示 untracked 文件,而 autoload/net.tscn 正是新文件。

godot-ai MCP 使用注意(重要)

  • autoload 的全局标识符在「正在运行的编辑器会话」里不可见:新加的 autoload,脚本里直接写 Net.xxx 会报 Identifier not found: Net。改用 get_node_or_null("/root/Net")。
  • Godot 在 Windows 上不往重定向的 stdout 写日志(_console.exe 更糟,它自己开控制台)。抓输出必须用引擎参数 --log-file <path>。
  • game_manage.get_ui_elements / get_scene_tree 看不到 /root 下的 autoload 节点,只认当前场景树。
  • 验证资源是否真被解析:写 res://tests/test_*.gd(继承 McpTestSuite,方法名 test_*,用 assert_true/assert_eq/assert_gt),再 test_run。