Files
agame/.workbuddy-ai/memory/2026-10-02.md
T
2026-10-02 10:34:57 +08:00

8.8 KiB
Raw Blame History

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。要不要减速/压低碰撞盒等用户定。

重做:第三人称 → 第一人称控制器

player.gd + player.tscn 改造。test_run 21/21,实机确认 CameraRig.position.y=1.6、Body.visible=false。

  • 场景:删掉 SpringArm,Camera 从 CameraRig/SpringArm/Camera 挪到 CameraRig/Camera。 CameraRig 变成「眼睛」,架在 eye_height 高,只转 yaw;pitch 在 Camera 自身上原地抬头低头。 fov 82.2 → 75(82 是给第三人称的,第一人称太鱼眼)。
  • player.gd 新增两个导出:eye_height(1.6,按 1.8 胶囊定,与模型高矮无关)、 hide_own_body(true,只藏本机那一份,远端照常显示)。不藏的话相机就在模型里/贴头顶。
  • 删掉 turn_speed:第一人称里身体不再朝移动方向转身,改成始终跟视线一致(_apply_look() 里 body.rotation.y = _yaw),这个平滑参数没用了。
  • 新增 _apply_look()(3 处调用:鼠标、滑索、_ready):鼠标一动就落节点,视角跟手不等物理帧。
  • pitch 范围从 -80/45 放到 ±89(第一人称要能抬头低头)。滑索的视角对齐沿用原来的 3.0 慢速率。

已知遗留:HealthLabel(y=2.05) 和 InteractHint(y=1.75) 是玩家自己位置的 Label3D, 第一人称下平视看不到(都在相机所在的竖直平面上,超出 FOV),但抬头会看到自己的血条 —— 要干净的话得改成 CanvasLayer 的 HUD(会连带改 test_zipline 里断言 Player/InteractHint 的那条)。

MCP 限制:game_manage(op="input_mouse") 注入的 InputEventMouseMotion 不带 relative 增量, 所以鼠标转视角没法用 MCP 验证(input_key/input_action 是正常的,用 H 键把血条打到 90 验证过)。

方案:面部贴图切换 + 游戏内自绘(只出方案,未写文件)

用户给了 mouse_表情测试.png,问怎么在游戏内切面部贴图、并让玩家自己画。

先探了模型(临时探针读各 mesh 的 surface/UV/材质,跑完删了):面部早就被拆成了独立 surface —— body_mesh s1 "face",UV 孤岛 u[0.142,0.356] v[0.038,0.238],自带材质和贴图。 所以不用改 Blender,直接 set_surface_override_material(1, ...)。

给的方案:面部 shader(把孤岛 UV 重映射到 0~1,ALBEDO = mix(base_tex, paint_tex, paint.a))

  • base_tex = 预设方形 PNG(切换就换它)+ paint_tex = 一个 512² 透明 SubViewport 的笔画层。 画布只装笔画不装底图,所以橡皮擦掉笔画露出来的是预设脸,语义正确。

关键设计点(也是给之前那份第三方方案纠错的地方):绘画 UI 是可见的 CanvasLayer + SubViewportContainer,输入天然可达 —— 不是把 SubViewport 当后台渲染目标(那样 _input 收不到事件)。

遗留待用户拍板:现有预设要不要裁成方形(重映射后必须裁,1024² 里裁 x145..365 y32..251); 联机时脸要不要 RPC 同步合成后的 PNG。