debug和修复
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
# 2026-10-10
|
||||
|
||||
## 新增右上角调试面板(DebugPanel)
|
||||
|
||||
**需求**:右上角显示玩家移动速度、相机高度等信息。
|
||||
|
||||
**新增文件**
|
||||
- `ui/debug_panel.gd`(`class_name DebugPanel extends Control`)—— 单节点 `_draw()` 实现,
|
||||
不用 Label 节点。理由:要自己控制两列对齐(标签左对齐、数值左对齐、列间距固定),
|
||||
不必去猜字体度量。总刷屏用 `draw_rect` 画底板。
|
||||
|
||||
**显示内容**(10 Hz 刷新,`refresh_interval` 可调)
|
||||
| 行 | 内容 |
|
||||
|---|---|
|
||||
| FPS | `Engine.get_frames_per_second()` |
|
||||
| 移动速度 | 水平速度 m/s + 身长/秒 |
|
||||
| 垂直速度 | `velocity.y` |
|
||||
| 相机高度 | 相机相对脚底的世界高度 m + 倍身高 |
|
||||
| 状态 | 站立/蹲下/疾跑 · 地面/空中 |
|
||||
| 位置 | 世界 X, Z |
|
||||
| 手持 | 拖拽中 / 可抓取 / — |
|
||||
|
||||
**关键设计点**
|
||||
- 相机高度用 `_camera.global_position.y - _player.global_position.y`(世界 y 相减),
|
||||
**不是**读 `CameraRig.position.y` —— 后者是本地坐标还要乘玩家缩放,且相减法对
|
||||
「蹲伏过渡中途」这种非稳态也直接成立。
|
||||
- `body_height`(默认 0.15)只用于换算「身长/秒」和「倍身高」,**和 `player.tscn` 的
|
||||
`Player.scale` 绑定,改角色尺寸时要同步改**。
|
||||
- `mouse_filter = IGNORE` 在 `_ready()` 里显式设置(见下方 MCP 限制),
|
||||
否则这个全屏 Control 会吃掉所有鼠标事件,鼠标视角和拖拽全会废掉。
|
||||
- `visible` 默认 false,`setup()` 里才置 true —— 避免远端玩家的「回放身体」
|
||||
闪出一块「未接线」面板。
|
||||
- F3 切换显隐(`toggle_key`)。`Key` 类型可以直接 `@export`。
|
||||
- 挂在 `CameraRig/HUD` 下(和准心同级)→ 远端玩家的 HUD 随 `camera_rig.queue_free()` 一起释放,
|
||||
天然只有本地玩家显示。
|
||||
|
||||
**改动文件**
|
||||
- `character/player/player.tscn`:`HUD` 下新增 `DebugPanel (Control)`,
|
||||
锚点 Full Rect(`anchor_right`/`anchor_bottom` = 1.0),挂 `ui/debug_panel.gd`。
|
||||
- `character/player/player.gd`:加 `@onready var debug_panel: DebugPanel = $CameraRig/HUD/DebugPanel`
|
||||
和 `_ready()` 里 `debug_panel.setup(self, motor, pickup, camera)`。
|
||||
- `character/player/player_pickup.gd`:新增公开 getter `is_hovering()`(配 `has_object()`)。
|
||||
|
||||
---
|
||||
|
||||
## ⚠ 本次踩到的 Godot MCP 限制(重要,可复用)
|
||||
|
||||
### 1. 编辑器在 play 模式时 MCP 拒绝一切写入
|
||||
`EDITOR_NOT_READY / EDITOR_PLAYING`。必须先 `project_manage(op="stop")`。
|
||||
(本次用户选择「我来停」,停完 `editor_manage(op="state")` 确认 `readiness: ready`。)
|
||||
|
||||
### 2. MCP 写不了整型属性
|
||||
`node_set_property` 传整数会变成 String → `WRONG_TYPE: Cannot write String to a int property`。
|
||||
**浮点/字符串正常**。受影响的:`mouse_filter`、`grow_horizontal`、`anchors_preset`。
|
||||
→ 绕过办法:
|
||||
- 锚点:改设 `anchor_right` / `anchor_bottom` = **1.0**(浮点可写);编辑器保存时会自动补
|
||||
`anchors_preset = 15` + `layout_mode = 3`,效果与准心节点完全一致。
|
||||
- 整型语义(如 `mouse_filter`):**写进脚本里**(`_ready()` 里设),别依赖场景属性。
|
||||
|
||||
### 3. 假 parse error(本项目的顽固问题,第二次遇到)
|
||||
`script_create` / `script_patch` / `filesystem_manage(write_text)` 的 `diagnostics` 会对
|
||||
**完全合法的文件**报 `gdscript_reload_failed, error_code 43 (ERR_PARSE_ERROR)`,
|
||||
`line` 是 `fallback_line: true`(≈ 文件总行数,不可信)。
|
||||
本次 `ui/debug_panel.gd`、`player_pickup.gd` 都被这样误报过,而真实项目上
|
||||
`--check-only` 全部通过。**判定:这是编辑器热重载产物,不是语法错误。**
|
||||
教训:**MCP 的 diagnostics 只能当提示,不能用它判定脚本坏了。**
|
||||
|
||||
### 4. 可靠的脚本语法校验方法(离线,不污染项目)
|
||||
Godot 可执行文件路径:
|
||||
`D:\_yvmou\Godot\Godot_v4.7.2-stable_mono_win64\Godot_v4.7.2-stable_mono_win64.exe`
|
||||
(从 `Get-Process | Where ProcessName -like "*godot*" | Select Path` 拿到)
|
||||
|
||||
直接对真实项目跑(安全,`--check-only` 解析完即退):
|
||||
```bash
|
||||
"$GODOT" --headless --path "E:/Godot/agame" --check-only --script "res://ui/debug_panel.gd"
|
||||
```
|
||||
**输出为空 = 通过**。
|
||||
|
||||
沙箱法(想彻底不碰项目时):临时目录建最小 `project.godot`,拷入脚本及其依赖,
|
||||
先 `--editor --quit` 生成 `.godot/global_script_class_cache.cfg`(否则会误报
|
||||
「Could not find type」——注意**类型解析失败也是报 Parse Error**),再 `--check-only`。
|
||||
|
||||
### 5. 字体(中文能否渲染)
|
||||
实测:`ThemeDB.fallback_font` **就是**默认主题字体(`FontFile` / "Open Sans SemiBold"),
|
||||
`allow_system_fallback = true`,系统有 Microsoft YaHei/SimSun。
|
||||
注意 `has_char("点")` 返回 **false**(`has_char` 不查系统回退链),但排版时回退生效:
|
||||
「移动速度相机高度」8 字量出 128px = 16px/字,字形确实取到了。
|
||||
→ **结论:`_draw()` + `ThemeDB.fallback_font` 画中文是可行的,不必额外挂字体资源。**
|
||||
|
||||
### 6. 其他
|
||||
- `custom_manage(op="list")` 可列出第三方 addon 注册的自定义工具。
|
||||
- `editor_manage(op="state")` 能看 play 状态;`logs_read(source="editor")` 只看得到
|
||||
脚本 reload 的 warning,拿不到 GDScript 的详细 parse 报错文本(stderr 到编辑器控制台)。
|
||||
- 用户 `world/main2.tscn` 是编辑器常驻场景,用完要**还原回去**(本次已还原)。
|
||||
|
||||
---
|
||||
|
||||
## `player_sync.tres` 为什么在 Godot 里「不可编辑」
|
||||
|
||||
- `SceneReplicationConfig` **没有任何 `@export` 属性**,类上只有 `add_property` / `remove_property` /
|
||||
`property_set_*` 这些**方法** → 通用 Inspector 渲染出来是空的,所以双击 `.tres` 看起来「不能编辑」。
|
||||
- 这不是错觉:Godot 4.7 二进制里有 `ReplicationEditor` / `Replication Dock` / `Remove Property` /
|
||||
`Replication Mode`(下拉枚举 `Never,Always,On Change`)/ `ReplicationInterface`,
|
||||
但**没有** `SceneReplicationConfigEditor` —— 即不存在独立的资源编辑器。
|
||||
- **正确改法**:选中场景里的 `Sync` 节点(`MultiplayerSynchronizer`)→ Inspector 的 Replication 区
|
||||
→ 添加/删除属性、勾 Spawn、选 Replication Mode → 保存场景(外部 .tres 会被一并写盘)。
|
||||
- 运行时可在调试器里看同步情况(`ReplicationInterface` / `ReplicationProfiler`)。
|
||||
|
||||
### 顺带发现:`.:health` 是失效路径
|
||||
`character/player/player.gd` 上**没有** `health`(`health` 只存在于 `demo/` 那套
|
||||
`agent_base.gd` + `Health` 节点)。同类「路径过期」问题已第三次出现
|
||||
(前两次是 `Body/AnimationPlayer:anim_state` / `anim_speed`)。
|
||||
当前 5 条里 4 条有效:`.:position`、`Body:rotation`、`AnimationPlayer:anim_state`、`AnimationPlayer:anim_speed`。
|
||||
|
||||
原 5 条全是 `replication_mode = 1`(Always,每处理帧持续发)。`anim_state` / `anim_speed`
|
||||
是低频变化量,改用 `On Change`(2,可靠传输、变化才发)更省带宽。
|
||||
|
||||
### 已处理(用户确认后动手)
|
||||
`player_sync.tres` 现为 4 条:
|
||||
|
||||
| # | 路径 | spawn | mode |
|
||||
|---|---|---|---|
|
||||
| 0 | `.:position` | true | 1 Always |
|
||||
| 1 | `Body:rotation` | true | 1 Always |
|
||||
| 2 | `AnimationPlayer:anim_state` | true | 2 On Change |
|
||||
| 3 | `AnimationPlayer:anim_speed` | true | 2 On Change |
|
||||
|
||||
- **改法**:MCP `filesystem_manage(write_text)` 直接写 .tres 文本 → `scan()` → `reimport()`。
|
||||
这个资源**没有编辑器 UI,也不存在「编辑器打开着」的竞态**,所以写文本是安全的
|
||||
(与 .tscn 的情况不同)。`reimport` 对 .tres 只会刷新文件条目(`skipped_non_imported`)。
|
||||
- **验证手段(值得复用)**:把手写的 `.tres` 拷进最小沙箱工程,用
|
||||
`--headless --path <沙箱> --script res://check.gd` 跑一段 `extends SceneTree` 的脚本,
|
||||
`load()` 它并打印 `get_properties()` / `property_get_spawn()` /
|
||||
`property_get_replication_mode()` —— 这是**唯一能验证手写 .tres 真能解析**的办法。
|
||||
实测输出:4 条,mode = 1,1,2,2 ✓
|
||||
- `resource_manage(op="load")` 对 `SceneReplicationConfig` 只返回 3 个属性
|
||||
(`resource_local_to_scene` / `resource_path` / `resource_name`),
|
||||
`op="inspect"` 直接拒绝(不在它支持的原生资源族里)。
|
||||
- `Always`(1) 用不可靠传输、每帧持续发;`On Change`(2) 用可靠传输、值变了才发。
|
||||
连续变化的量(position / rotation)用 `Always` 是对的,低频量才换 `On Change`。
|
||||
|
||||
### 决定:保持外部 .tres,不内嵌(用户明确选择「不改」)
|
||||
调研过三条替代路,全部放弃,**以后不必再提**:
|
||||
- **A 内嵌 `sub_resource`**:功能零损失、可少一个文件,但给配置加条目要调 `add_property()`
|
||||
—— 那是**方法不是属性**,MCP 只能读写属性(`custom_manage` 也返回 0 个自定义工具),
|
||||
最后 4 行只能用户在 Replication 面板手点。不值得为省一个文件付这个手工成本。
|
||||
- **B 脚本里构造**(`MultiplayerSynchronizer` 上挂脚本,`_ready()` 里 new 一个 config 再赋给
|
||||
`replication_config`):会让编辑器 Replication 面板与实际同步内容**脱节**,
|
||||
且运行时赋值能否赶在同步器开始工作前生效**未实测**。放弃。
|
||||
- **C 弃用 Synchronizer 改 `@rpc`**:`world/main.tscn` 用 `MultiplayerSpawner` + `main.gd` 的
|
||||
`spawn_function` 生成玩家,「新 peer 拿到当前动画状态」是 `spawn = true` 白送的;
|
||||
换 RPC 要自己补初始状态 / 节流 / 丢包策略。为 4 个值不划算。
|
||||
- 根本原因:`.tres` 的"缺点"只是不能双击打开,而**编辑入口一直是 Sync 节点的 Replication
|
||||
面板**(换成 A 也是同一个面板)。
|
||||
|
||||
### `/Player/Sync` 节点实测值
|
||||
`root_path = ".."`(默认值 = 父节点 Player,这就是 `.:position` 指 Player 的原因)、
|
||||
`delta_interval = 0.0`、`public_visibility = true`、`visibility_update_mode = 0`。
|
||||
`replication_config` **不在** `node_get_properties` 可读属性里(被 ReplicationEditor 面板接管)。
|
||||
|
||||
### 补充:外部 .tres 改动的落盘机制(用户追问「tres 不是不可编辑吗」)
|
||||
- 准确说法:`player_sync.tres` **没有自己的编辑界面**,但**内容可以改**,入口是 Sync 节点的
|
||||
Replication 面板。这是两件事,别混。
|
||||
- 根因链:`SceneReplicationConfig` 没有任何 `@export` 属性(只有方法)→ 通用 Inspector 渲染不出
|
||||
可编辑项 → Godot 只好把专用 UI(`ReplicationEditor`)挂在**使用它的节点**
|
||||
(`MultiplayerSynchronizer`)上。实测证据:`resource_manage(op="load")` 只回 3 个元数据属性
|
||||
(`resource_local_to_scene` / `resource_path` / `resource_name`)—— 这就是你双击它看到的全部内容。
|
||||
- **编辑器加载的那份 .tres 实例就是项目文件本身**,所以面板里改它会**写回 `.tres`**,重启后仍在
|
||||
(与 `duplicate()` 出来的副本不同 —— 这既是它"生效"的原因,也是 Godot 著名的「资源被意外共享
|
||||
修改」陷阱的同一机制)。改完记得保存场景。
|
||||
- 真正做不到的只有一件事:**手改文本时没有校验**。NodePath 写错编辑器不会拦 ——
|
||||
`.:health`、`Body/AnimationPlayer:anim_state` 两次事故都源于此。要改路径请走面板下拉选,
|
||||
别手打。
|
||||
|
||||
### 项目事实:资源类的三种来源(排查中确认)
|
||||
- `demo/ai/trees/*.tres` 的类型是 `BehaviorTree` / `BlackboardPlan`,**但这两个类不在任何 .gd 里** ——
|
||||
`addons/limboai/` 只有 `bin/*.so`(GDExtension,C++ 编译产物),一个 `.gd` 都没有。
|
||||
所以它们是**插件用 C++ 注册的 Resource 类**,用起来和引擎内置类没区别(LimboAI 的 C++ 版)。
|
||||
- 项目里目前**没有任何自己写的 `extends Resource` 脚本**(排除 addons)。
|
||||
已存在的 `.tres` 类型:`StandardMaterial3D`(素材包材质)、`SceneReplicationConfig`(player_sync)、
|
||||
`BehaviorTree`(limboai)。
|
||||
- 没有二进制 `.res`。
|
||||
- 判据:**`@export` 就是「能不能在 Inspector 里编辑」的分水岭**。`SceneReplicationConfig` 一个
|
||||
`@export` 都没有 → 打开是空的;自定义 Resource 有 → 双击就能编辑。
|
||||
|
||||
### 调试器面板截图排查(15:58 用户贴图)
|
||||
**三条警告**都是历史遗留、与近期改动无关,来自项目加载时的 `GDScript::reload` 全量重载:
|
||||
- `face_painter.gd:153` 整数除法 —— **不是 bug**。`_stamp_d` 恒为偶数(第 138 行 `d = r * 2`),
|
||||
整除不丢精度。要消警告用 `@warning_ignore("integer_division")` + 注释说明,**不要**改成浮点。
|
||||
- `face_rig.gd:190` 局部变量 `name` 遮蔽 `Node.name` → 改名(如 `entry_name`)即可。
|
||||
- `paint_panel.gd:69` 局部变量 `scale` 遮蔽 `Control.scale` → 改名(如 `factor`)即可。
|
||||
|
||||
**那条错误 `net.gd:66 host_game(): Couldn't create an ENet host.` 也不是 bug**:
|
||||
日志证据显示当时**两个实例同时在跑** —— 主机(peer 1)已占住 7000 端口,第二个实例点「创建主机」
|
||||
必然失败(这是预期路径),随后它点「加入」**成功连上**(日志里有完整握手 + 双方各自生成角色)。
|
||||
功能正常,唯一可改进的是错误消息不够人话。
|
||||
|
||||
排查要点:**`user://logs/godot.log` 是「按项目」共享的,多实例会并发写同一个文件**,
|
||||
所以日志行会交错(截图里 `enet_connection.cp` 被从中间截断就是并发写的痕迹)。
|
||||
判据:`[Net] 本端 1 …` 和 `[Net] 本端 1484785418 …` 同时出现 = 两个进程。
|
||||
|
||||
### ⚠ 两条重要的方法论修正(都靠实测证伪)
|
||||
|
||||
**1. 之前说的「假 parse error」成因找到了 —— 不是假错,是热重载场景内的脚本。**
|
||||
`script_patch`/`script_create` 返回 `gdscript_reload_failed (error_code 43)` 的文件,
|
||||
全部都是**挂在「当前编辑器打开的那个场景」里的节点上的脚本**:
|
||||
`debug_panel.gd`(HUD/DebugPanel)、`player_pickup.gd`(/Player/Pickup)、
|
||||
`face_painter.gd`、`face_rig.gd`(FaceRig) —— 当时开着的正是 `player.tscn`。
|
||||
而 `paint_panel.gd`(在 paint_panel.tscn 里)、`net.gd`(autoload,net.tscn 没开)
|
||||
**都返回 `diagnostics: []` 干净结果**。
|
||||
结论:场景里有活实例时热重载会被判定失败,**文件本身没问题**。
|
||||
想拿干净诊断:先把编辑器切到一个**不含该脚本实例**的场景,或直接用下面的离线校验。
|
||||
|
||||
**2. `--check-only` 只报错、不报警告!**
|
||||
实测探针(`var name := "hello"` + `5 / 2`,两条确定会触发的警告)→ `--check-only` **零输出**。
|
||||
所以「无输出」只能证明**没有语法/分析错误**,**不能**作为「警告已消除」的证据。
|
||||
要确认警告消掉了,必须看编辑器的 Errors 面板(重启编辑器或重新加载项目)。
|
||||
本次三条警告的处理依据是「遮蔽变量已改名 + 所有引用点都改到」(`face_rig.gd` 的 `name`
|
||||
在 190/191/192/202/208 共 5 处,**漏改后两处会静默解析成 `Node.name`、不报错但值全错**,
|
||||
已全部改名 `entry_name`)。
|
||||
Reference in New Issue
Block a user