# 2026-10-02 ## 审查:Godot 4.7「SubViewport 面部手绘 + 骨骼驱动」方案 用户贴来一份第三方方案(SubViewport 当可绘制面部贴图 + SkeletonModifier3D 把骨骼 Z 转成 shader 参数),问可行性。只做评审,未改任何代码。 结论:**原理可行**(SubViewport-as-texture + shader 混合是标准做法),但原文代码有 4 处硬伤: 1. shader `render_mode ... skip_vertex_transform` 却没写 vertex() 手动变换 → 模型位置错。删掉该 render_mode。 2. `PaintSurface._input` 在**后台 SubViewport 里收不到鼠标事件**(Viewport 之间不派发输入)→ 必须放进 `SubViewportContainer`,或手动 `push_input`。 3. `SkeletonModifier3D` **没有 `skeleton` 属性**,只有 `get_skeleton()`(已查 4.5 文档确认)。 4. `save_paint` 没 `await RenderingServer.frame_post_draw` → 存到空帧。 标定类问题:`get_bone_pose_position()` 是相对父骨的姿态,Z 轴受父骨朝向影响,GLTF 导入后与 Blender 不一致,z_min/z_max 必须实测标定;`face_index` 只做了亮度乘,没真正切换表情图层。 与本项目相关的判断:角色是 `character-male-e.glb`,需先确认有独立面部材质槽 + 展平 UV;多人下 SubViewport/材质必须每实例一份,远端玩家的画要按 PNG 字节流同步。 ## 重写 player_animator.gd(7 状态) 按新枚举 `{IDLE, WALK, CROUCH, RUN, FALL, SNEAK, JUMP}` 重写 `character/player/player_animator.gd`。 改动:加 `class_name PlayerAnimator`(`player.gd` 用它当类型)、新增 `Stance{STAND,CROUCH,SNEAK}`、 `set_motion(velocity, on_floor, sprinting=false, stance=STAND)`、片段基准速度扩到 walk/run/crouch/sneak。 实测结论(用 @tool 副本跑 McpTestSuite,14 条断言全过): - 短片段名查不到,必须用 `"库名/片段名"`(`walk/walk`);`_ready()` 里扫库解析一次并写入 `_clips`。 - 状态映射、自动绑兄弟 AnimationPlayer、`play("walk/walk")`、loop_mode 补丁、倍率 5/5≈1.0 均正确。 - `class_name` 之后 `self is AnimationPlayer` 是编译期错误 → 脚本不能挂在 AnimationPlayer 节点上。 **未改、留给用户决定的场景问题**(`player.tscn`): 1. 脚本目前挂在 `Body/AnimationPlayer` 上,而 `player.gd`、`player_sync.tres`、`tests/test_net_replication.gd` 都找 `Body/PlayerAnimator` —— 需要新增独立 PlayerAnimator 节点。 2. 该节点的 6 个 float 导出在 .tscn 里被序列化成 `null`(实测运行时是 `Nil`),会覆盖脚本默认值。 3. `tests/test_net_replication.gd` 里的 `REQUIRED_CLIPS` 仍是旧的 `sprint`,需要同步成新状态集。 ## 收尾:按用户决定接线 + 清场景脏数据 用户决定「脚本就挂在 `Body/AnimationPlayer` 上」,据此改完并验证(`test_run` 20/20,320 条断言;实机跑 player.tscn 确认 idle→fall 切换正常)。 - `player_animator.gd`:文档改成「脚本挂在 AnimationPlayer 上、`animation_player` 留空自绑自身」。 - `player.gd`:`$Body/PlayerAnimator` → `$Body/AnimationPlayer`,场景结构注释同步;`_ride_zipline` 里 `tan` 改名 `tan_dir`(消 built-in 遮蔽警告)。 - `player_sync.tres`:同步字段路径改 `Body/AnimationPlayer:anim_state` / `:anim_speed`。 - `player.tscn`:删掉被序列化成 `null` 的 6 个 float 导出(`blend_time` 等)—— 实测运行时确实是 `Nil`,会覆盖脚本默认值。 - `tests/test_net_replication.gd`:路径、`REQUIRED_CLIPS` 全部同步;新增 `_find_clip()` 按库解析(短名断言会假红)。 - `tests/test_zipline.gd`:`test_interact_action_exists` 先调 `InputMap.load_from_project_settings()` —— 编辑器进程的 InputMap 不带项目动作(实测 `interact`/`jump` 均为 false,只有 `ui_accept`),这条一直假红。 - `world/zipline/zipline.gd`:`tan` → `tan_dir`。 **Godot MCP 注意**:`node_get_properties` 读不到脚本的 @export 属性(`unknown_fields`),但 `node_set_property` 能写进去;数值标量经 MCP 会变成字符串(`node_set_property` 返回 `"0.15"`),`batch_execute` 里的 `set_property` 直接报 INTERNAL_ERROR。改 `.tscn` / `.tres` 这类被编辑器占用的文件,直接用文本工具写反而更省事(`.tscn` 可写、`.tres` 报 EBUSY,用 `filesystem_manage(op="write_text")`),写完 `scene_open(force_reload=true)` 让编辑器重新读盘。 ## 接线:Ctrl 蹲下 / Ctrl+移动 潜行 用户定的规则:`crouch` 绑 Ctrl;**蹲着不动 = CROUCH,蹲着走 = SNEAK**。 - `project.godot`:用 `input_map_manage(op="ensure_binding", {action:"crouch", event_type:"key", keycode:"Ctrl"})` 加的动作,落在 keycode 4194326,和已有的 `sprint`(Shift/4194325) 同一套写法。 - `player_animator.gd`:**删掉 `Stance` 枚举**,`set_motion` 第 4 个参数改成 `crouching: bool`。 「蹲 / 潜行」的区分放回表现层(`moving := 水平速度 >= idle_threshold`),控制器只传「按没按 Ctrl」。 优先级改成 空中 > 蹲 > 移动。 - `player.gd`:调用点传 `Input.is_action_pressed("crouch")`;操作说明补上 Ctrl。 - `tests/test_net_replication.gd`:加 `test_crouch_action_exists`(先 `InputMap.load_from_project_settings()`)。 验证:`test_run` 21/21;另用 @tool 副本跑了 4 组状态映射断言(Ctrl+不动→CROUCH、Ctrl+移动→SNEAK、 Ctrl+Shift→仍是 SNEAK、空中→JUMP/FALL 优先于蹲),片段也确认切到了 `crouch/crouch` / `sneak/sneak`。 **踩坑**:`test_run` 的脚本缓存按路径,删掉重建同名文件会跑旧版本 —— 第一次探针就因此假失败 (旧版 `stance: int` 收到 `true` 被当成 `Stance.CROUCH`)。换文件名才拿到真实结果。 **未做**:蹲下目前不改移动速度、也不改碰撞胶囊高度 —— 潜行时用走速(5 m/s)播 sneak 片段, `speed_scale` 会被 clamp 到上限 1.6。要不要减速/压低碰撞盒等用户定。