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

126 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`。