- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
156 lines
13 KiB
Markdown
156 lines
13 KiB
Markdown
# movement.keyboard-motion
|
||
|
||
## Scope
|
||
|
||
比较键盘输入、WASD 快速切换、移动请求、方向/速度计算、动作选择和停止清理。
|
||
|
||
## Reference call chain
|
||
|
||
- `UserInterface/PythonPlayerInputKeyboard.cpp`
|
||
- `UserInterface/PythonPlayerInput.cpp`
|
||
- `UserInterface/InstanceBaseMovement.cpp`
|
||
- `UserInterface/InstanceBaseMotion.cpp`
|
||
- `GameLib/ActorInstanceMotion.cpp`
|
||
- `GameLib/ActorInstanceMotionEvent.cpp`
|
||
|
||
## Current call chain
|
||
|
||
- `project/player_controller.gd`
|
||
- `project/net_play.gd`
|
||
- `project/net_world.gd`
|
||
- `project/test_wasd_steering_parity.gd`
|
||
- `project/test_no_auto_move_regression.gd`
|
||
- `project/player_motion_test.gd`
|
||
- `project/keyboard_motion_timeline_test.gd`
|
||
|
||
## Audit findings (2026-09-20)
|
||
|
||
40250 的键盘路径是事件驱动的:`NEW_SetSingleDIKKeyState` → `NEW_SetSingleDirKeyState` 更新四个方向位 → `NEW_SetMultiDirKeyState`。无方向立即 `NEW_Stop`;有方向按 `Up > Down`、`Left > Right` 计算八方向角,再调用 `NEW_MoveToDirection`。`NEW_MoveToDirection` 依次执行取消点地预约、主角存在/私店/锁定/移动技能/同步门、设置 `m_isGoing = FALSE`、设置 advancing rotation、启动 walk/run motion,并把当前位置向量加 300 像素目标。
|
||
|
||
当前端已按事件维护方向位,W/S 优先级、释放切换、失焦、进场残留和直立旋转均有回归证据;本轮新增 `keyboard_motion_timeline_test.gd`,实际驱动 `InputEventKey` 和 `_process`,覆盖“加载期按键不继承 → W+S 优先 → 释放 W 暴露 S → 释放最后方向停止”的时序。
|
||
|
||
仍存在明确的实现层差异,不能升级为 STATIC_VERIFIED:
|
||
|
||
- 40250 在按键事件中设置固定 300 像素 `Dst`,随后由 `.msa`/`CActorInstance::AccumulationMovement` 按动作根运动和 `m_fMovSpd` 推进;当前端在 `_process` 中按米制连续速度推进,模型可用时读取 `get_move_motion_speeds`,否则回退 `SPEED_WALK/SPEED_RUN`。方向角可一致,但目标距离、速度、到达时刻和阻挡后的状态不同。
|
||
- 40250 的 `CInstanceBase::Update → AccumulationMovement → Transform` 才会触发 `OnMove/OnMoving/OnWaiting/OnStop`;当前端 `moved` 和 `anim_state` 来自每帧预测位置,`NetPlay` 再按 0/300/100ms 节流发 `FUNC_MOVE/FUNC_WAIT`。已有节流测试,但没有证明每个动作资源停止/切换事件与网络状态事件一一对应。
|
||
- 40250 `NEW_MoveToDirection` 的 `__IsSyncing`、`isLock`、`IsUsingMovingSkill` 分支是“忽略移动但可能保留原动作”的同步门;当前端 `frozen`/`locked`/`moving_skill` 在 `_process` 早返回,`_goto` 被门挡时只对点地目标建立预约,键盘方向本身不建立预约。被击退、情绪、技能锁和解锁恢复的顺序尚未全部对齐。
|
||
- 40250 的相机旋转可在 `NEW_MoveToDirection` 内先改变相机再叠加角色方向;当前端读取 `camera.heading()` 后直接构造前后左右向量,没有实现同一套相机旋转副作用。
|
||
- 当前端用 Euler 分量强制保持 `rotation.x/z = 0`,解决了 W/S 快切翻跟头;这是有效的 Godot 平台适配,但还缺“角色根节点/骨骼动画旋转不被姿态资源覆盖”的真实模型测试。
|
||
|
||
因此现有测试证明的是输入回归和网络节流的已覆盖路径,不是完整 40250 移动/动作/资源链等价。
|
||
|
||
## Baseline tests
|
||
|
||
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、`netplay_test.gd` 和 `keyboard_motion_timeline_test.gd` 当前通过,证明已覆盖的回归路径没有复现自动移动、W/S 粘键、翻滚和 0/300/100ms 网络节流问题;这不替代完整 40250 分支等价审计。
|
||
|
||
## Deep audit round 2026-09-20
|
||
|
||
本轮重新对照 `NEW_SetSingleDirKeyState`、`NEW_MoveToDirection`、`AccumulationMovement`、`RotationProcess` 和当前 `PlayerController`,并重跑输入与动作测试:
|
||
|
||
- 当前 `active` 切换、W/S 优先级、KeyUp 全局接收、失焦清键、`rotation.x/z=0` 和跨 `±PI` 的单向转向均有可重复测试;这解释了此前 W/S 快切翻跟头已经被当前平台适配压住。
|
||
- 40250 方向键移动把目标设置为当前位置前方固定 300 像素,再由动作根运动和 `m_fMovSpd` 推进;当前端仍用 `_process` 的米制连续速度推进,并在模型缺失时使用固定速度 fallback。因此“方向一样”不代表到达时刻、动作结束事件和服务端 MOVE/WAIT 边沿一致。
|
||
- 当前 `frozen/locked/moving_skill` 在 `_process` 中早返回或只转向;40250 的同步、私店、受击、情绪、移动技能和 `NEW_Stop` 分支会分别决定是否清 `m_isGoing`、是否保留动作及何时恢复。本轮没有端到端证明这些门在解锁后与 40250 同一帧恢复。
|
||
- 当前摄像机方向只参与构造移动向量,没有复刻 40250 `NEW_MoveToDirection` 内相机旋转与角色 advancing rotation 的完整副作用;真实角色模型测试 `player_motion_test.gd` 虽打印 PASS,但本轮退出码为 139 并报告 6 个 ObjectDB 泄漏,不能作为干净证据。
|
||
- `test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、`keyboard_motion_timeline_test.gd` 本轮退出码 0,覆盖的输入回归通过;因此只保留“输入问题已回归通过”,不把根运动、资源事件、锁门和退出生命周期标记为等价。
|
||
|
||
结论:WASD 输入状态和翻转回归已稳定,但 40250 的固定目标/根运动、动作事件源、锁门恢复、相机副作用和真实模型退出安全仍有差异,合同保持 `PARTIAL`。
|
||
|
||
### Active UserInterface actor-transform review
|
||
|
||
- `InstanceBaseEvent.cpp` 将 `CInstanceBase` 的 event handler 直接绑定到 `CActorInstance`;`InstanceBaseTransform.cpp` 提供 cm 像素位置、rotation/blend direction、LookAt fly target/destination 和当前/advancing rotation 访问。
|
||
- `PythonPlayerEventHandler.h` 明确消费 `OnSyncing/Waiting/Moving/Move/Stop/Warp/ClearAffects/SetAffect/ResetAffect/Attack/UseSkill/Hit`,并在 bow fly handler 中把 `OnSetFlyTarget` 与动作帧 `OnShoot` 转成网络副作用。当前端 `PlayerController`/`NetPlay` 有移动/技能信号,但未证明远端/本地都按同一 event handler 顺序消费,自动移动和快速 WASD 仍需状态时序 oracle。
|
||
- 本轮完成 InstanceBase/PlayerEventHandler 静态核对,键盘合同继续为 `PARTIAL`。
|
||
|
||
## Implementation round 2026-09-20T19:03Z
|
||
|
||
本轮将键盘方向目标按 `InstanceBaseMovement.cpp::NEW_MoveToDirection` 的固定距离
|
||
规则收敛:方向事件现在记录当前角色位置加 300 像素(当前 Godot 单位为 3.0m)的
|
||
`_direction_dst`,后续帧只向该目标推进,目标到达后停止,不再每帧从当前位置延长
|
||
路径;移动技能仍保留只转向不平移的分支。死亡、受击/锁定门和方向释放会清理该目标。
|
||
|
||
新增的时间线断言验证了固定目标、到达停止、W/S 优先级、KeyUp 暴露、最后方向释放和
|
||
进入游戏不继承输入;`keyboard_motion_timeline_test.gd`、`player_move_test.gd`、
|
||
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd` 均退出码 0。
|
||
|
||
这只修复了目标创建/消费语义,不宣称完整根运动等价:当前端仍由 Godot `_process`
|
||
按速度推进,尚未接入 40250 `.msa`/`AccumulationMovement` 的真实动作事件、相机副作用、
|
||
同步/情绪/受击解锁恢复和完整 MOVE/WAIT 时间线,合同继续为 `PARTIAL`。
|
||
|
||
## Equivalence matrix
|
||
|
||
| 项目 | 状态 | 备注 |
|
||
|---|---|---|
|
||
| Preconditions | PARTIAL | active/失焦/按键残留、锁门、私店、情绪和锁门 held WASD 已覆盖;同步、受击、死亡和传送门未全覆盖 |
|
||
| Branch structure | PARTIAL | W/S、A/D、八方向和释放顺序已覆盖;相机旋转与所有禁止移动分支未闭合 |
|
||
| Algorithms/formulas | PARTIAL | 方向优先级和归一化一致;固定 300 像素 Dst 与根运动被连续速度替代 |
|
||
| State transition order | PARTIAL | 输入状态、锁门/移动技能恢复、PlayerController、NetPlay 入口已定位;资源事件与网络事件的完整顺序未证实 |
|
||
| Constants/units | PARTIAL | 角度/旋转阈值和 0/300/100ms 节流已记录;米/厘米、移动速度和目标距离不同 |
|
||
| Timing/event sources | PARTIAL | 当前是 `_process` 预测 + signal;参考是 Actor motion event 驱动 |
|
||
| Resource/data sources | PARTIAL | 当前模型速度可从动作资源读取但有 fallback;`.msa` event/AccumulationMovement 尚未一一对照 |
|
||
| Protocol side effects | PARTIAL | `FUNC_MOVE/FUNC_WAIT` 发送和节流有测试;阻止/重复/相机副作用仍未闭合 |
|
||
| Interruption/failure/cleanup | PARTIAL | stop、失焦、换图、锁定/移动技能恢复已覆盖;受击/情绪/传送和完整解锁矩阵仍需测试 |
|
||
|
||
## Remaining
|
||
|
||
- 用真实 `camera.heading()`、模型动作速度和网络 fake 记录同一方向序列的输入事件、移动目标、停止请求、动画状态和包时间线。
|
||
- 补充死亡/传送/受击以及私店、情绪、移动技能和普通锁定的多门叠加顺序;各单门 held WASD 恢复已覆盖基础路径。
|
||
- 继续对照 `.msa` 的 motion event、`AccumulationMovement` 根运动和当前 `get_move_motion_speeds` fallback,决定是否需要根运动适配层。
|
||
- 核对相机旋转副作用与角色方向叠加,以及真实角色模型在 W/S 快切时根节点/骨骼姿态不翻转。
|
||
|
||
### Audit round 2026-09-21
|
||
|
||
本轮重新执行 `keyboard_motion_timeline_test.gd`、`test_wasd_steering_parity.gd` 和
|
||
`test_no_auto_move_regression.gd`,三项均通过;W/S 优先级、KeyUp 暴露被按住方向、最后
|
||
方向释放停止、进入游戏不继承加载期按键、角色 X/Z 姿态归零、跨 `±PI` 转向和技能结束后
|
||
不自动恢复移动均有回归证据。
|
||
|
||
沿 `ActorInstanceBattle.cpp::ComboAttack`、`ActorInstanceMotion.cpp::AccumulationMovement`
|
||
和 `PythonPlayerEventHandler::OnMove/OnMoving/OnWaiting/OnStop` 再次核对后,没有把这些
|
||
输入测试升级为完整等价:当前端仍用连续速度预测替代固定 300 像素 Dst + 根运动,动作资源
|
||
事件与移动网络事件也不是同一来源;同步/受击/情绪/移动技能解锁恢复、相机旋转副作用和
|
||
真实模型根节点测试仍是下一轮差异项。合同保持 `PARTIAL`,本轮未改动实现。
|
||
|
||
### Implementation fix round 2026-09-21T11:45Z
|
||
|
||
本轮按 40250 `CPythonPlayer::Update` 在 `m_isDirKey` 下每帧重试
|
||
`NEW_SetMultiDirKeyState(m_isLeft,m_isRight,m_isUp,m_isDown)` 的语义修复了当前端的
|
||
锁门恢复差异:方向键仍按住时,`frozen/locked` 不再丢弃方向状态;移动技能接管移动时
|
||
会清除旧的平移目标但保留 held WASD,技能结束或锁定解除后的首个可移动更新重新创建
|
||
固定 300cm 目标。方向释放、死亡和停止仍会清除 pending 状态,避免旧目标在技能结束后
|
||
莫名续走。
|
||
|
||
`keyboard_motion_timeline_test.gd` 新增锁定恢复和移动技能恢复断言;与
|
||
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、
|
||
`player_motion_test.gd` 一起退出码均为 0。真实模型测试同时改为清理 native model/animation
|
||
节点并清空测试用的 GR2/MSA 缓存,确认本轮测试不再出现 ObjectDB 泄漏和 exit 139。
|
||
|
||
这轮只闭合了 held WASD 在锁门/移动技能边沿的状态恢复,不宣称完整移动等价:当前端仍
|
||
用 Godot 连续速度预测替代 `.msa`/`AccumulationMovement` 根运动,`OnMove/OnMoving/`
|
||
`OnWaiting/OnStop` 事件源、同步/受击/传送边界、相机副作用和完整网络时间线
|
||
仍保持 `PARTIAL`。
|
||
|
||
### Implementation fix round 2026-09-21T12:05Z
|
||
|
||
本轮补齐了 40250 `__CanMove` 的两个此前缺失的主角状态门:
|
||
`CPythonPlayer::IsOpenPrivateShop()` 与 `__IsProcessingEmotion()`。当前端新增
|
||
`PlayerController.set_private_shop_open()` / `set_processing_emotion()`;门开启时会
|
||
清理点地预约和旧方向目标,但保留 WASD 的方向位,和 `NEW_SetSingleDirKeyState`
|
||
保留 `m_isDirKey`、`CPythonPlayer::Update` 解锁后重新调用
|
||
`NEW_SetMultiDirKeyState` 的行为一致。`NetPlay._on_entity_info()` 只对主角读取
|
||
`shop_sign` / `acting_emotion`,因此查看或打开其他角色的商店不会锁住本地输入。
|
||
|
||
`keyboard_motion_timeline_test.gd` 新增摆摊中、情绪处理中“无平移且保留按键”,以及
|
||
状态解除后第一帧恢复移动的断言;本轮 targeted tests 通过。私店/情绪两个移动门已闭合,
|
||
但根运动、动作事件源、同步/受击/传送边界、相机副作用和完整网络时间线仍保持
|
||
`PARTIAL`。
|
||
|
||
## Audit round 2026-09-21T04:45Z — keyboard/root-motion boundary
|
||
|
||
本轮重新执行 `keyboard_motion_timeline_test.gd`、`test_wasd_steering_parity.gd`、
|
||
`test_no_auto_move_regression.gd` 和 `player_motion_test.gd`,全部退出码为 0:
|
||
|
||
- 进入游戏不继承加载期按键、W/S 优先与释放切换、最后方向停止、技能结束不续走、快速 WASD 不翻跟头、跨 `±PI` 单向转向和真实模型动作装配均保持通过。
|
||
- 继续对照 `NEW_SetMultiDirKeyState`、`NEW_MoveToDirection`、`AccumulationMovement` 和 `OnMove/OnMoving/OnWaiting/OnStop` 后,确认当前端仍是 Godot `_process` 连续速度预测,不能用输入测试替代 40250 `.msa` 根运动/动作事件源证据。
|
||
|
||
结论:本轮未修改实现;输入回归保持稳定,合同继续 `PARTIAL`。下一轮需要用真实动作资源和 fake network timeline 对拍固定 300cm 目标、根运动、停止事件和 MOVE/WAIT 包边沿。
|