抱起物品
This commit is contained in:
@@ -80,3 +80,54 @@
|
||||
|
||||
**教训**:改 `.gdshader` / `.tscn` 这类编辑器可能正开着的文件之后,最好提醒用户先关掉或重开那个文件再保存;
|
||||
否则他那边一保存就会把磁盘上的新版本覆盖掉,而且不会有任何提示。
|
||||
|
||||
## 实现:抱起物品系统(第 1 步,单机跑通)
|
||||
|
||||
用户要求:左键抱起、长按 Q 扔出、抱起后胳膊伸出去抱住且走路会晃、胳膊可延伸、只有特定物品能抱并给个示例。
|
||||
|
||||
新增/改动:
|
||||
- `world/items/carryable.gd`(`class_name Carryable extends RigidBody3D`,**标了 @tool**):
|
||||
门槛在物品自己身上 —— 进 `carryable` 组才算「可抱」。参数:`hold_offset` / `hold_rotation_deg` /
|
||||
`freeze_while_carried` / `throw_speed_min` / `throw_speed_max`。
|
||||
`begin_carry()` 冻结物理并**把 collision_layer 清 0**(不清会把抱着它的人顶飞);`end_carry(v)` 恢复并给初速度;
|
||||
`apply_hold_transform(socket)` 由持有者每帧调用,偏移/朝向由物品自己决定。
|
||||
- `world/items/carryable_box.tscn`:示例物品(0.6³ 橙箱子,RigidBody3D + Mesh + Collision + 脚本)。
|
||||
- `character/player/carry_arms.gd`(`class_name CarryArms extends Node`,也标了 @tool):
|
||||
抱持姿势 + 走路晃动的表现层。**动画之后叠加**(`process_priority = 100`),只动 arm_l_*/arm_r_* 六根骨骼,
|
||||
晃动同时作用在骨骼和抱持点上,所以人和东西一起晃。
|
||||
- `player.gd`:`_find_carryable / _pick_up / _release_throw` 三个小函数;交互提示与判定同源(优先级:手上有东西 > 可抱物 > 缆上 > 可挂的缆);抱东西时禁疾跑 + `carry_speed_scale` 降速;抱着时不给挂滑索。
|
||||
- `player.tscn`:新增 `CameraRig/CarrySocket`(Marker3D,本地 (0,-0.8,-1.0)) 和 `Body/CarryArms`。
|
||||
- `world/main.tscn`:放两个示例箱子。
|
||||
- 输入动作:`carry` = 鼠标左键、`throw_carry` = Q(`input_map_manage ensure_binding`,直接落 project.godot)。
|
||||
- `tests/test_carry.gd`:7 条(动作存在、玩家场景节点、示例物品可抱、抱起/放下冻结与恢复、贴到抱持点、投掷速度随蓄力、抱持姿势真的伸到身前)。
|
||||
|
||||
踩到的坑:
|
||||
- **`CarryArms` 的 NodePath 少了一级**:默认写成 `mouse/MouseRig/Skeleton3D`,但节点挂在 `Body` 下,
|
||||
应该是 `../mouse/MouseRig/Skeleton3D`;抱持点同理要 `../../CameraRig/CarrySocket`。
|
||||
解析不到只会 push_warning,姿势完全不生效、画面看不出原因。
|
||||
- **骨骼本地轴不能猜**:一开始按骨骼本地欧拉角写,结果手是往**上后方**摆的。
|
||||
改成「世界方向语义」——参数按世界方向理解,内部用 `rest_basis.inverse() * world * rest_basis`
|
||||
换算到骨骼本地空间,再乘到动画姿态上。这样换模型也不会错。
|
||||
- 参数是扫出来的(临时探针一次跑 24 组 swing/tuck/roll,打印 `arm_l_03` 的全局位置):
|
||||
最终 `shoulder_swing_deg = 40 / tuck = 50 / roll = 0 / elbow = 15 / stretch = 1.25`
|
||||
→ 手落在模型本地 (±1.17, 0.08, 0.95),即身前中部。
|
||||
- **非 @tool 脚本在编辑器里是 placeholder**,测试里方法调不动、`_ready` 也不跑(组也加不上)。
|
||||
`carryable.gd` / `carry_arms.gd` 都标了 `@tool` 才跑得通;改完脚本要 `filesystem_manage(op="scan")`
|
||||
才会重新解析(不 scan 的话测试还按旧代码跑)。
|
||||
- 顺手纠正:脸部贴花的正面是模型**本地 +Z**(尾巴在 -Z),我之前写成 `-normalize(object_normal).z` 是错的,
|
||||
已改回 `+`,`tests/test_face.gd` 的断言也跟着反过来。
|
||||
|
||||
验证:`test_run` **39/39**;实机跑通完整链路 —— 提示「左键 抱起」→ 左键抱起(箱子出现在身前)→
|
||||
长按 Q(提示「长按 Q 扔出 100%」)→ 松手箱子飞出去落地。
|
||||
|
||||
**未做**:联机同步(第 2 步)。现在物品位置只有持有者本地在写,远端玩家看不到箱子被抱走 —— 需要给物品加
|
||||
`MultiplayerSynchronizer` + 跟随持有者的 authority 切换 + 抢占仲裁。
|
||||
|
||||
## 给「抱起物品系统」出了方案(未动代码)
|
||||
|
||||
用户要新功能:玩家抱起物品,先要方案。我在对话里给了设计,没有写文件。
|
||||
要点:`Carryable` 组件挂在物品上(组 `carryable`,照抄 Zipline 的「参数放组件、玩家只读」套路);
|
||||
玩家侧只加 `_find_carryable/_pick_up/_drop` 三个小函数 + `CameraRig/CarrySocket` 标记点;
|
||||
交互优先级和提示文案共用一处 `_resolve_interaction()`(滑索踩过「提示亮着但按 E 没反应」的坑);
|
||||
抱持**不 reparent**,改成 `carried_by` 状态 + 持有者每帧摆到 socket,authority 随持有者切换;
|
||||
分三步:单机跑通 → 联机仲裁/同步 → 持物动画与扔出。等用户拍板几个点(E 是否复用、要不要真物理/扔、模型有没有持物动画)。
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
**FaceRig 故意不传这几个 uniform** —— 早期版本从 `@export` 传过去,会把 shader 里调好的值盖掉;现在 `face_rig.gd` 里没有对应的 export,测试里有一条断言禁止再传。
|
||||
- 贴图第 0 行在上边,而头部 UV 的 v 朝上,所以默认必须 `face_flip_y = true`,否则表情上下颠倒(用户实测确认过)。
|
||||
- 内置表情在各自图集 tile 里的墨线占比:`face_01` 68.9%×74.4%、`face_02` 83.9%×65.4%,且都居中 —— 所以 `face_size` 要按「墨线只占 tile 的七成」来估:4.0 的矩形给出约 2.8 单位的墨线宽度。
|
||||
- `test_face_decal_inside_body_bounds` 校验贴花矩形落在模型包围盒里(模型换尺度/原点会红);`test_face_shader_has_expected_uniforms` 里钉住 `-normalize(object_normal).z` 和 `object_position.xy`,防止有人改回 UV 方案或写反正面。
|
||||
- `test_face_decal_inside_body_bounds` 校验贴花矩形落在模型包围盒里(模型换尺度/原点会红);`test_face_shader_has_expected_uniforms` 里钉住 `normalize(object_normal).z`(**不能带负号**,正面是本地 +Z)和 `object_position.xy`,防止有人改回 UV 方案或把正面写反。
|
||||
- 两个 FaceRig 节点(`player.tscn` 的 `Player/FaceRig`、`character_select.tscn` 的预览 `FaceRig`)都不带表情参数 —— 值只有一份,在 shader 里。
|
||||
- `FaceRig` 保存 `user://face_preset.json` 中的 expression ID 和 `user://face_paint.png` 暂存笔画;已保存的自绘表情用 `user://face_library/index.json` + 各自透明 PNG 持久化,角色选择页动态显示内置 atlas 表情和本地表情。
|
||||
- 联机当前表情同步:内置表情发送 ID;`user:` 本地表情发送当前 PNG 字节;笔画层另外发送。**不广播整份本地表情库**。
|
||||
@@ -71,6 +71,31 @@
|
||||
- 「自己画」= 空白底起手;保存到本地表情库成为独立项目,再次选择可加载。取消会恢复旧 expression ID 与笔画。
|
||||
- 界面链路:`ui/main_menu.tscn` →「开始游戏」→ `ui/character_select.tscn`(3D 预览 + 动态表情列表 + 自己画)→ `ui/paint_panel.tscn`(透明笔画层叠在肤色底上)。本地列表用 HFlow + ScrollContainer 显示。
|
||||
|
||||
## 抱起物品(world/items/ + character/player/carry_arms.gd)
|
||||
|
||||
- **门槛在物品自己身上**:物品根节点(RigidBody3D)挂 `Carryable`(`class_name`,进 `carryable` 组)才抱得起来,
|
||||
玩家侧不认识任何具体物品。手感和滑索一个套路 —— `hold_offset` / `hold_rotation_deg` / `throw_speed_min/max`
|
||||
都写在物品上,玩家只负责「找最近的、摆到抱持点、扔出去」。
|
||||
- 示例物品:`world/items/carryable_box.tscn`(0.6³ 橙箱子),世界场景里放了两个。加新物品只要挂脚本。
|
||||
- 玩家侧只有 `_find_carryable / _pick_up / _release_throw` 三个函数;**交互提示和按键判定同源**
|
||||
(优先级:手上有东西 → 可抱物 → 缆上 → 可挂的缆),别再拆成两处。
|
||||
- 抱持**不 reparent**:`Carryable.carried_by` 记状态,持有者每物理帧 `apply_hold_transform(socket)`。
|
||||
抱起来时冻结物理 + **collision_layer 清 0**(不清会把玩家顶飞),放下时恢复并继承初速度。
|
||||
- 输入动作:`carry` = 鼠标左键、`throw_carry` = Q(长按蓄力,松手出手)。鼠标没锁定时左键仍然只用来重新锁定视角。
|
||||
- 抱东西时禁疾跑 + `carry_speed_scale` 降速;**抱着不给挂滑索**(`_attach_zipline()` 开头直接 return)。
|
||||
- 抱持点 `CameraRig/CarrySocket`(Marker3D,本地 (0,-0.8,-1.0))。物品位置要跟着它走。
|
||||
- `CarryArms`(挂在 `Body/CarryArms`)是**表现层**:`process_priority = 100` 保证跑在 AnimationPlayer 之后,
|
||||
在动画姿态上**叠加**手臂旋转(只动 `arm_l_*` / `arm_r_*`),走路时胳膊和抱持点一起晃。
|
||||
**参数按「世界方向」语义**(`rest_basis.inverse() * world * rest_basis` 换算到骨骼本地空间)——
|
||||
直接写骨骼本地欧拉角会得到「手往上后方摆」这种莫名其妙的结果,换模型必错。
|
||||
默认 swing 40 / tuck 50 / roll 0 / elbow 15 / stretch 1.25 → 手落在模型本地 (±1.17, 0.08, 0.95)。
|
||||
- 本地玩家第一人称藏了自己的模型(`hide_own_body`),所以**自己看不到自己的胳膊**,晃动只有别的玩家能看到;
|
||||
想自己看就把 `hide_own_body` 关掉并把 `eye_height` 降下来。
|
||||
- **未做**:联机同步。现在物品位置只有持有者本地在写,远端看不到箱子被抱走 ——
|
||||
要给物品加 `MultiplayerSynchronizer`、authority 随持有者切换、再加抢占仲裁。
|
||||
- 测试 `tests/test_carry.gd`:`Carryable` / `CarryArms` 都标了 `@tool`,否则编辑器里是 placeholder、
|
||||
方法调不动、`_ready` 不跑(组也加不上);改完脚本要 `filesystem_manage(op="scan")` 才重新解析。
|
||||
|
||||
## Godot 4 踩坑
|
||||
|
||||
- **canvas 没有「擦除」混合模式**:`CanvasItemMaterial.BlendMode` 只有 mix/add/sub/mul/premult,
|
||||
@@ -110,7 +135,9 @@
|
||||
- **非 `@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` 里验非 @tool 脚本,**最省事的做法是直接给脚本加 `@tool`**(`Carryable` / `CarryArms` 就是这么办的,
|
||||
运行时行为完全一样),其次才是临时生成一个 @tool 副本喂给测试。
|
||||
加完要 `filesystem_manage(op="scan")` 一次,否则编辑器还在用旧的非 tool 版本,照样报 placeholder。
|
||||
- **`test_run` 的 GDScript 编译缓存是按路径的**:删掉再重建同名脚本,跑到的可能还是旧版本
|
||||
(现象:逻辑明明改了,测试却按旧逻辑失败)。要验新代码就换个文件名,或者重启编辑器。
|
||||
- **编辑器进程的 `InputMap` 只有内建动作**(`ui_accept` 有,`interact` / `jump` 没有)——
|
||||
|
||||
Reference in New Issue
Block a user