15 KiB
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:新增公开 getteris_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 解析完即退):
"$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)。