12 KiB
12 KiB
项目长期记忆
工程约定
- 玩家控制器只认「运动结果」,不认识任何动画片段名;表现层脚本
player_animator.gd直接挂在Body/AnimationPlayer上(animation_player留空即自绑自身,所以两者是同一个节点)。 新增动作状态改ANIM_CLIPS,不要往player.gd里塞片段名。 - 组件的可调参数放在组件自己身上(如 Zipline 的速度 / 刹车 / 吸附半径),控制器只读不定义。
- 头顶 UI(血条、交互提示)是
player.tscn里的 Label3D 节点,不在代码里动态创建。 - 测试用
McpTestSuite(addons/godot_ai/testing),脚本放res://tests/。 - 界面放
res://ui/。项目没有 Theme 资源,风格一律用节点上的theme_override_*内联写; 给容器(如 VBoxContainer)设theme_override_styles/*会向下继承给子按钮,不用逐个设。 - 中文字体不用额外配:Godot 默认主题会回退到系统字体,实测中文正常渲染。
- 入口场景是
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)
- 多 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, 这个区分在表现层做,控制器不需要知道两者差别。优先级:空中 > 蹲 > 移动。
模型:mouse.glb
- 2026-10-02 用户重建过模型:原来
body_mesh的「lambert1身体 +face面部」两个材质槽已合并成 一个材质mat_body,贴mouse_body_basecolor.png(纯色底)。耳朵单独ear_l_mesh/ear_r_mesh用mat_ear+mouse_ear_basecolor.png;手臂、腿、尾巴是另外的 mesh 节点(mat_arm/mat_leg/mat_body)。 旧的两张贴图mouse_mouse_lambert1_BaseColor.png、mouse_表情测试.png已被删除,别在代码或测试里再引用。 body_mesh现在 1 个 surface、484 顶点、6 段 UV 孤岛,UV 整体落在 u[0.004,0.996] v[0.259,0.996]。 最高处那段孤岛(= 头部)的 UV 是 u[0.695,0.996] v[0.682,0.996];没有任何一段专门的「脸」孤岛, 头部是一整块连续展开。脸部矩形必须整块落在头部孤岛里,见tests/test_face.gd的test_face_uv_rect_inside_head_island。- 量 UV 孤岛的办法:临时写一个
@tool extends McpTestSuite的探针脚本,用并查集把 index buffer 连成 连通分量,再打印每段的 3D AABB + UV bbox;也可以把 UV 画成 64×32 的#/.占据图。跑完记得删掉脚本。
面部系统(character/player/face/ + ui/)
face_paint.gdshader:将 face surface 的 UV remap 到 0~1,再映射到内置图集 rect 或本地 PNG;贴图 alpha 只代表墨线覆盖率,混到skin_color上,不写空间ALPHA(否则脸片变洞)。mouse_mouse_lambert1_BaseColor.png和mouse_表情测试.png的背景都精确为 RGB(137,179,255) /#89B3FF;表情图源 alpha 全 255,不能直接当透明度。- 最终做法:全身一个 shader,脸部靠 UV 矩形圈出来。
face_paint.gdshader用body_tex(=mat_body的 BaseColor)采样全身底色,再在face_uv_min/face_uv_size圈出的矩形里,把表情贴图(内置图集或本地 PNG)和玩家手绘按 alpha 叠上去。透明处保持身体底色,所以脸和周围不可能是两块不同的皮肤。FaceRig把 mat_body 的albedo_texture/albedo_color/roughness/metallic抄进 shader,并用set_surface_override_material(0, mat)(surface 0,不再是 1)。 - 不再需要
skin_color当 shader 输入:FaceRig反过来从身体贴图左上角取色写进skin_color,只给画布和缩略图垫底用(导入贴图是压缩的,get_pixel前要先decompress(),否则报 "Can't get_pixel() on compressed image")。 - 表情遮罩用
step()相乘而不是if:采样留在统一控制流里,免得 mipmap 导数在分支里未定义。 - 仍然不能写空间
ALPHA:alpha 只代表墨线覆盖率,写进 ALPHA 会让面片变洞。 face_atlas_transparent.png是我们自己的素材(从已删除的mouse_表情测试.png去蓝底、反混合出来的 1024² 副本),保留原图坐标排布;内置 IDbuiltin:face_01/02对应各自的图集 rect。原始表情图已随模型删除,这个副本现在是唯一来源,别删。- 脸部矩形 = 整块头部 UV 孤岛 (0.695, 0.682) 尺寸 (0.301, 0.314) —— 用户确认「就是整块头部」。头部 UV 的 v 越大越靠上,而贴图第 0 行在上边,所以必须开
face_uv_flip_y(默认 true),否则表情上下颠倒(用户实测确认过)。想往下挪:把face_uv_min.y钉在孤岛底边 0.682、改小face_uv_size.y;min.y 不能低于 0.682,否则矩形会伸进下方那段孤岛(u 0.6950.935 / v 0.2930.674,是胸腹),表情会同时画到肚子和前胸上。 face_uv_min/face_uv_size/face_uv_flip_y在player.tscn和character_select.tscn里各有一份(两个 FaceRig 节点),检视器里改只影响当前那个;要两边一致就改face_rig.gd里的默认值。face_uv_size是Vector2。测试test_face_uv_rect_inside_head_island会按 min/max 归一化后再判定,允许负边长当翻转用。FaceRig保存user://face_preset.json中的 expression ID 和user://face_paint.png暂存笔画;已保存的自绘表情用user://face_library/index.json+ 各自透明 PNG 持久化,角色选择页动态显示内置 atlas 表情和本地表情。- 联机当前表情同步:内置表情发送 ID;
user:本地表情发送当前 PNG 字节;笔画层另外发送。不广播整份本地表情库。 FacePainter:直接改 RGBA8 字节缓冲。画 = source-over alpha 混合,擦 = 把目标 alpha 乘掉;512² 一次 1MB,按一笔/动作同步一次。- 「自己画」= 空白底起手;保存到本地表情库成为独立项目,再次选择可加载。取消会恢复旧 expression ID 与笔画。
- 界面链路:
ui/main_menu.tscn→「开始游戏」→ui/character_select.tscn(3D 预览 + 动态表情列表 + 自己画)→ui/paint_panel.tscn(透明笔画层叠在肤色底上)。本地列表用 HFlow + ScrollContainer 显示。
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"; 而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,比手改文件稳。