# 项目长期记忆 ## 工程约定 - 玩家控制器只认「运动结果」,不认识任何动画片段名;表现层脚本 `player_animator.gd` 直接挂在 `Body/AnimationPlayer` 上(`animation_player` 留空即自绑自身,所以两者是同一个节点)。 新增动作状态改 `ANIM_CLIPS`,不要往 `player.gd` 里塞片段名。 - 组件的可调参数放在组件自己身上(如 Zipline 的速度 / 刹车 / 吸附半径),控制器只读不定义。 - 头顶 UI(血条、交互提示)是 `player.tscn` 里的 Label3D 节点,不在代码里动态创建。 - 测试用 `McpTestSuite`(`addons/godot_ai/testing`),脚本放 `res://tests/`。 ## 动画(AnimationPlayer) - **多 AnimationLibrary 下,片段必须用「库名/片段名」的限定键**。实测(4.7.2): `has_animation("idle")` 为假,`has_animation("idle/idle")` 才为真,`get_animation` 同理。 本项目每个 glb 导入成一个库、库名与片段同名,所以真键形如 `walk/walk`。 短名播不出来且不报错,是典型的静默失效 —— `PlayerAnimator._ready()` 里扫库解析一次。 - 导入的 GLB 片段 `loop_mode` 默认是 `NONE`,编辑器里改不了,只能运行时补。 - **带 `class_name` 的脚本挂在 AnimationPlayer 上没问题,但不能写 `self is AnimationPlayer`**: `class_name X extends Node` 之后 `self` 的静态类型就是 X,那句是编译期错误。 要拿「自己就是播放器」这个事实得绕一圈:遍历 `get_parent().get_children()` 找 `is AnimationPlayer` 的兄弟 —— 自己就在里面。见 `PlayerAnimator._ready()`。 - 状态集 `IDLE/WALK/CROUCH/RUN/FALL/SNEAK/JUMP`。签名 `set_motion(velocity, on_floor, sprinting, crouching)`: 第 4 个参数是「按住蹲键」这个输入事实(`crouch` 动作 = Ctrl),蹲着不动 → CROUCH、蹲着走 → SNEAK, 这个区分在表现层做,控制器不需要知道两者差别。优先级:空中 > 蹲 > 移动。 ## Godot MCP 踩坑 - **数值标量经 MCP 会被字符串化**:`node_set_property` 传 `0.15` 能写进去,但回显是 `"0.15"`; 而 `batch_execute` 里包 `{"command": "set_property", "params": {...}}` 直接报 `INTERNAL_ERROR … handler returned malformed result`(和旧版本文档里写的正好相反,以实测为准)。 改被编辑器占用的 `.tscn` / `.tres` 时别绕这个弯 —— 直接写文本更省事:`.tscn` 可写, `.tres` 报 EBUSY 就改用 `filesystem_manage(op="write_text")`。 - **`node_get_properties` 读不到脚本的 @export 属性**(会列进 `unknown_fields`),但 `node_set_property` 能写。 改完场景文件要 `scene_open(force_reload=true)` 让编辑器重读盘,否则节点上还挂着旧脚本实例。 - **@tool 脚本现造的节点不会写进 .tscn**:`add_child` 后不给 `owner`, `PackedScene::pack()` 就会跳过它。缆绳网格因此可以纯由锚点推导,不存在「文件和锚点对不上」的脏数据。 - **新 `class_name` 要跑一次 `filesystem_manage(op="scan")`** 才进全局类表, 否则别的脚本把它当类型引用会解析不到。 - `game_manage(op="suspend")` 期间**输入事件不送达**,`next_frame` 也拦不住物理帧; 想抓「运行中途某一瞬间」的画面不可靠,只能靠日志 / 属性断言验证。 - 测试跑在编辑器进程里,`@tool` 节点必须真的 `add_child` 进编辑中场景才会跑 `_ready()`; 用完在 `teardown()` 摘掉,节点名以 `_McpTest` 开头可被测试运行器兜底清理。 - **非 `@tool` 脚本在编辑器里是「placeholder instance」**:`load(path).can_instantiate()` 返回 false, `.new()` 报 `Nonexistent function 'new' in base 'GDScript'`,`set_script` 后调方法报 `Attempt to call a method on a placeholder instance`(`player.gd` 也一样,不是脚本本身的问题)。 想在 `test_run` 里验非 @tool 脚本,就临时生成一个「去掉 class_name、加上 @tool」的副本喂给测试,跑完删掉。 - **`test_run` 的 GDScript 编译缓存是按路径的**:删掉再重建同名脚本,跑到的可能还是旧版本 (现象:逻辑明明改了,测试却按旧逻辑失败)。要验新代码就换个文件名,或者重启编辑器。 - **编辑器进程的 `InputMap` 只有内建动作**(`ui_accept` 有,`interact` / `jump` 没有)—— 项目动作要等游戏进程启动时才 load 进去。所以断言 `InputMap.has_action(...)` 的测试在编辑器里必然假红, 先调一次 `InputMap.load_from_project_settings()`。 - 改输入动作走 `input_map_manage(op="ensure_binding", params={action, event_type="key", keycode="Ctrl"})`, 幂等且直接落 `project.godot`,比手改文件稳。