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

124 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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。
## 新建:游戏开始菜单(只有界面,无逻辑)
`ui/main_menu.tscn` + `ui/main_menu.gd`(新目录 `ui/`)。
- 背景用 `GradientTexture2D` 取游戏天空的三段色(zenith 0.09/0.30/0.78 → mid → horizon),
和 `world/sky/cartoon_sky.gdshader` 的调色一致。
- 结构:`MainMenu(Control) → Background(TextureRect) / Center(CenterContainer) → Box → Title + Buttons`,
右下角一个 `Version` Label。
- 四个按钮 开始游戏 / 联机 / 设置 / 退出,**只有 `pressed` → 自定义信号**,没有任何开始/退出/联机实现。
- 按钮样式用 3 个 `StyleBoxFlat`(normal/hover/pressed)**挂在 `Buttons` 这个 VBoxContainer 上**,
靠主题继承一次性作用到四个按钮,不用逐个写。
- 实测(跑起来截图 + `get_ui_elements`):版式正确、中文正常渲染。
**发现**:`Net` autoload 的「联机测试」面板默认可见且在同位置,会把菜单中间的按钮盖住 ——
接线时必须先处理它(隐藏,或让菜单的「联机」按钮接管)。这条已记进 MEMORY.md。