开始菜单、玩家选择、脸部绘制
This commit is contained in:
@@ -121,3 +121,77 @@ SubViewportContainer`,输入天然可达** —— 不是把 SubViewport 当后
|
||||
**发现**:`Net` autoload 的「联机测试」面板默认可见且在同位置,会把菜单中间的按钮盖住 ——
|
||||
接线时必须先处理它(隐藏,或让菜单的「联机」按钮接管)。这条已记进 MEMORY.md。
|
||||
|
||||
## 实现:角色选择 + 脸部预设选择 + 脸部绘制
|
||||
|
||||
按之前的方案落地了。`test_run` **26/26 通过**(含新增的 face suite 5 条),实机跑通并截图确认。
|
||||
|
||||
**新增**
|
||||
- `character/player/face/face_paint.gdshader` — 孤岛 UV 重映射 + `mix(base, paint, paint.a)`
|
||||
- `character/player/face/face_painter.gd` — 笔画层,直接改 RGBA8 字节缓冲
|
||||
- `character/player/face/face_rig.gd` — 挂 surface 1 的材质、切预设、存读档(`user://face_*.json/png`)
|
||||
- `character/player/face/presets/face_01.png` `face_02.png` — 从 `mouse_表情测试.png` 裁的 512² 方形预设
|
||||
- `ui/paint_panel.tscn/.gd` — 绘画面板(颜色 / 粗细 / 橡皮 / 清空 / 取消 / 完成)
|
||||
- `ui/character_select.tscn/.gd` — 3D 预览(自转)+ 预设缩略图 + 自己画
|
||||
- `tests/test_face.gd` — 钉住「surface 1 叫 face、UV 孤岛落在 shader 的框里、预设是正方形」
|
||||
|
||||
**改动**:`player.tscn` 加 `FaceRig` 节点;`main_menu.gd`「开始游戏」→ 角色选择,并把 Net 调试面板收起来。
|
||||
|
||||
**踩到的坑(都补进 MEMORY.md 了)**
|
||||
1. Godot 4 的 canvas **没有擦除混合模式**,`draw_set_blend_mode` 也没了 → 橡皮改成自己乘 alpha。
|
||||
2. `Image.set_data()` 在 4.7 变成 5 个参数 → 改用 `Image.create_from_data`。
|
||||
3. 导出的**节点引用**在场景实例化时解析成 null → 改成 `mesh_path: NodePath` + `_ready()` 里 `get_node_or_null`。
|
||||
4. shader 里 `vec2` 不能用 `source_color`(只给 vec3/vec4/sampler)。
|
||||
5. 画布一开始只显示笔画层,等于让玩家对着空气画 → 底下垫一层预设 TextureRect。
|
||||
6. 拖拽判定原本看 motion 的 `button_mask`,MCP 注入的 motion 不带这个字段 → 改成自己记 `_drawing`。
|
||||
7. 排查时一度以为画笔没生效,其实黑点画在脸的黑色线条上被淹没了 —— 换到纯蓝区域才看见。
|
||||
诊断顺序记一下:先 `print` 确认输入到了(`[PP]`)→ 再确认印章落了(`[FP] stamp`)→ 再看缓冲字节(`probe a=255`)。
|
||||
|
||||
## 收尾:入口换成开始菜单 + 联机脸同步
|
||||
|
||||
`test_run` **28/28 通过**。两件事都用实机验证过(入口流程用 MCP 点按钮走通;脸同步用**两个
|
||||
headless 实例** `--host` / `--join 127.0.0.1` 真连了一次)。
|
||||
|
||||
**入口场景**
|
||||
- `project.godot` 的 `run/main_scene` 改成 `res://ui/main_menu.tscn`(用 `project_manage(set_main_scene)` 改的)。
|
||||
- `net.gd`:加 `WORLD_SCENE` / `MENU_SCENE`、`_enter_world()`、`_go_to()`、`set_menu_visible()`;
|
||||
`host_game()` 和 `_on_connected_to_server()` 之后切进世界;`leave_game()` 切回菜单并把 `_spawner` 置空。
|
||||
- **关键**:世界是开局后才加载的,`bind_spawner()` 必须补生成已经连上的玩家,否则主机在菜单里
|
||||
开好服、进世界时没有角色。
|
||||
- `main_menu.gd`:「确定」→ `Net.host_game()`;用 `Net.set_menu_visible()` 代替原来的节点路径 hack。
|
||||
- 新增测试 `test_main_scene_is_the_menu`(防有人把入口改回世界)。
|
||||
|
||||
**脸同步**
|
||||
- `FaceRig` 加三个方法:`_pull_remote_face` / `_rpc_request_face` / `_rpc_send_face`。
|
||||
**用「谁缺谁来要」而不是出生时推** —— 各端玩家节点出现的先后顺序不保证,推的话很可能推给一个
|
||||
还不存在的节点。非本机玩家在 `_ready` 里主动去要,带重试。
|
||||
- `FacePainter` 加 `to_png_bytes()` / `set_image()`,走内存不落盘。
|
||||
- **踩到的坑**:第一次跑双实例,客户端刷了一屏
|
||||
`Node not found: "Main/<id>/FaceRig"` / `Failed to get path from RPC` ——
|
||||
主机生成玩家比客户端早几帧,主机立刻去要时客户端还没建出那个节点。
|
||||
加 `FACE_SYNC_INITIAL_DELAY = 1.0`(首次要之前先等一秒)后两边都干净了。
|
||||
- 验证结果:HOST 和 JOIN 都打出「收到 peer X 的脸,png=1930 字节」,且无任何报错。
|
||||
1930 正好是 `user://face_paint.png` 的大小,说明笔画真的传过去了。
|
||||
- 新增测试 `test_face_sync_methods_exist`(钉住三个方法名)。
|
||||
|
||||
**找到 Godot 可执行文件**:`D:\_yvmou\Godot\Godot_v4.7.2-stable_win64.exe\Godot_v4.7.2-stable_win64_console.exe`
|
||||
(注意那一串带 .exe 的其实是个**目录**)。双开调试命令:
|
||||
`<godot> --headless --path E:/Godot/agame -- --host` / `-- --join 127.0.0.1`。
|
||||
|
||||
## 修 bug:点「自己画」不是空白画布
|
||||
|
||||
用户报的。根因是**语义理解错了**:我原先把「自己画」做成「在选中的预设上补几笔」,
|
||||
用户要的是**从空白脸起手**。
|
||||
|
||||
- 生成 `presets/face_00_blank.png`(512² 纯色 `#89B3FF`,取自预设贴图里脸的底色)。
|
||||
- `character_select.gd`:「自己画」= 清掉缩略图高亮 + `_paint_panel.open(_rig, BLANK_FACE)`。
|
||||
- `paint_panel.gd`:`open()` 多一个 `blank_face` 参数,**先拍快照再换底图 + 清笔画**。
|
||||
- **顺带修了一个连带 bug**:`_on_cancel` 原来只还原笔画,而「自己画」会把底图换成空白 ——
|
||||
点取消会留下一张白脸。现在底图(`_preset_before`)也一起还原。
|
||||
- 移掉了 `_on_paint_done`(高亮在 `_on_draw` 里已经清了,那个回调成了死代码)。
|
||||
|
||||
验证:实机走了一遍「选预设 → 自己画(画布变纯色、预览里脸也变纯)→ 取消(底图还原回 face_01)」,
|
||||
并用 `get_node_info` 确认两个缩略图 modulate 都回到 0.62。`test_run` 28/28。
|
||||
|
||||
**一个排查小教训**:截图里 Preset0 看着比 Preset1 亮,我一度以为高亮没清掉 ——
|
||||
其实两张贴图本身明暗就不同,`modulate` 才是真相。**别靠截图判断颜色,去读属性。**
|
||||
|
||||
|
||||
@@ -11,8 +11,15 @@
|
||||
- 界面放 `res://ui/`。项目没有 Theme 资源,风格一律用节点上的 `theme_override_*` 内联写;
|
||||
给容器(如 VBoxContainer)设 `theme_override_styles/*` 会**向下继承**给子按钮,不用逐个设。
|
||||
- 中文字体不用额外配:Godot 默认主题会回退到系统字体,实测中文正常渲染。
|
||||
- **`Net` 是 autoload,它的「联机测试」面板在 `_ready()` 里就 `visible = true`(CanvasLayer layer=20)**,
|
||||
会盖在任何场景正中间。做开始菜单时要么让菜单的「联机」按钮接管它,要么在菜单期间把它藏起来。
|
||||
- **入口场景是 `res://ui/main_menu.tscn`**(不再是世界)。流程:开始菜单 →「开始游戏」→ 角色选择 →
|
||||
「确定」= `Net.host_game()` → Net 切到 `world/main.tscn`;「联机」= 放出 Net 的创建/加入面板,
|
||||
选完同样由 Net 切进世界。**进世界 / 回菜单这两件事都归 Net 管**(`_enter_world` / `leave_game`),
|
||||
菜单只管「按了哪个按钮」。
|
||||
- **世界是开局之后才加载的**,所以 `MultiplayerSpawner` 的绑定时机跟着变了:
|
||||
`Net.bind_spawner()` 里要把已经连上的玩家补生成一遍,否则主机在菜单里就开好服、
|
||||
切进世界时一个角色都不会出现。
|
||||
- `Net` 是 autoload,它的「联机测试」面板在 `_ready()` 里就 `visible = true`(CanvasLayer layer=20),
|
||||
会盖在菜单正中间 —— 菜单调 `Net.set_menu_visible(false)` 收起来。
|
||||
|
||||
## 动画(AnimationPlayer)
|
||||
|
||||
@@ -40,6 +47,34 @@
|
||||
- 面部孤岛只占 1024² 的 21%×20%,实际脸分辨率约 219×205 —— 要用 shader 把孤岛重映射到 0~1
|
||||
再用方形小贴图,有效分辨率能翻一倍多。
|
||||
|
||||
## 面部系统(character/player/face/ + ui/)
|
||||
|
||||
- `face_paint.gdshader`:把孤岛 UV 重映射到 0~1,`ALBEDO = mix(base_tex, paint_tex, paint.a)`。
|
||||
**两层必须分开**:`base_tex` = 预设脸(切换就换它),`paint_tex` = 玩家笔画层。
|
||||
这样橡皮擦掉笔画露出来的是预设脸,而且换预设不会丢手绘。
|
||||
- `FaceRig`(挂在 Player 下):建一张 512² 的笔画 Image + ImageTexture,`set_surface_override_material(1, mat)`。
|
||||
存 `user://face_preset.json`(选了哪张预设)+ `user://face_paint.png`(透明底笔画层),
|
||||
玩家出生时 `apply_saved()` 读回来。预览和玩家是**两套 FaceRig、共用一份存档**。
|
||||
- `FacePainter`:**不用 `Control._draw()`,直接改 RGBA8 字节缓冲**。画 = source-over alpha 混合,
|
||||
擦 = 把目标 alpha 乘掉;一笔画完 `_sync()` 一次(512² 一次 1MB,别每盖一个印章就同步)。
|
||||
- 界面链路:`ui/main_menu.tscn` →「开始游戏」→ `ui/character_select.tscn`(3D 预览 + 预设缩略图 + 自己画)
|
||||
→ `ui/paint_panel.tscn`。**画布要「预设 TextureRect + 笔画 TextureRect」叠着显示** ——
|
||||
笔画层是透明底的,只显示它等于让玩家对着空气画。
|
||||
- **「自己画」= 从空白脸起手**(`presets/face_00_blank.png`,纯色底),不是在预设上补笔。
|
||||
空白由面板来落(`open(rig, blank_face)`)—— 它必须**先拍快照再改**,否则点「取消」还原回来的
|
||||
是已经清空的状态。取消要同时还原**底图和笔画**,只还原笔画会留下一张白脸。
|
||||
|
||||
## Godot 4 踩坑
|
||||
|
||||
- **canvas 没有「擦除」混合模式**:`CanvasItemMaterial.BlendMode` 只有 mix/add/sub/mul/premult,
|
||||
`CanvasItem.draw_set_blend_mode()` 在 4.x 已移除。橡皮只能自己管像素。
|
||||
- `Image.set_data()` 在 4.7 签名变了(要 5 个参数);改用
|
||||
`Image.create_from_data(w, h, mipmaps, format, data)`。
|
||||
- **导出的节点引用解析不到**:`@export var mesh: MeshInstance3D` 在 .tscn 里存成 `NodePath`,
|
||||
实测场景实例化时拿到的是 null。改成 `@export var mesh_path: NodePath` + `_ready()` 里 `get_node_or_null`。
|
||||
- shader 里 `source_color` 只能用在 vec3 / vec4 / sampler 上,`vec2` 用了会编译失败。
|
||||
- 给容器(VBoxContainer 等)设 `theme_override_styles/*` 会**向下继承**给子按钮,不用逐个设。
|
||||
|
||||
## Godot MCP 踩坑
|
||||
|
||||
- **数值标量经 MCP 会被字符串化**:`node_set_property` 传 `0.15` 能写进去,但回显是 `"0.15"`;
|
||||
|
||||
Reference in New Issue
Block a user