fix(movement): camera auto-rotates to face WASD input direction
40250 parity: CPythonPlayer::NEW_MoveToDirection's m_isCmrRot branch continuously rolls the camera toward the held movement direction every frame while a direction key is down, independent of whether a new translation target was just set. Add _camera_auto_rotate() to replicate this (forward/back = no rotation, strafe = max rate, diagonal = half rate), called unconditionally after the frozen/locked/shop/emotion gate in _process(). Godot's yaw-increase handedness is opposite the reference's D3D Roll(), so the sign is flipped as a platform adaptation; verified by convergence invariants in keyboard_motion_timeline_test.gd rather than bit-for-bit angle matching. Closes movement.keyboard.motion/camera-auto-follow-rotation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c93894313a
commit
7df97e01f9
@@ -12,10 +12,15 @@
|
||||
- `UserInterface/InstanceBaseMotion.cpp`
|
||||
- `GameLib/ActorInstanceMotion.cpp`
|
||||
- `GameLib/ActorInstanceMotionEvent.cpp`
|
||||
- `GameLib/GameUtil.cpp`(`CameraRotationToCharacterRotation`/`CharacterRotationToCameraRotation`)
|
||||
- `EterLib/Camera.cpp`(`CCamera::Roll`/`CalculateRoll`,增量式)
|
||||
- `GameLib/ActorInstanceBattle.cpp`(`CanAct`/`CanMove`)
|
||||
- `GameLib/ActorInstance.cpp`(`IsParalysis`/`IsFaint`/`IsSleep`)
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/player_controller.gd`
|
||||
- `project/game_camera.gd`
|
||||
- `project/net_play.gd`
|
||||
- `project/net_world.gd`
|
||||
- `project/test_wasd_steering_parity.gd`
|
||||
@@ -81,8 +86,8 @@
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | active/失焦/按键残留、锁门、私店、情绪和锁门 held WASD 已覆盖;同步、受击、死亡和传送门未全覆盖 |
|
||||
| Branch structure | PARTIAL | W/S、A/D、八方向和释放顺序已覆盖;相机旋转与所有禁止移动分支未闭合 |
|
||||
| Algorithms/formulas | PARTIAL | 方向优先级和归一化一致;固定 300 像素 Dst 与根运动被连续速度替代 |
|
||||
| Branch structure | PARTIAL | W/S、A/D、八方向、释放顺序和 `m_isCmrRot` 相机跟随已覆盖;Paralysis/Faint/Sleep 未接入移动门 |
|
||||
| Algorithms/formulas | PARTIAL | 方向优先级、归一化和相机跟随的 fold-to-±90/rate 公式一致(`movement.keyboard.motion/camera-auto-follow-rotation`);固定 300 像素 Dst 与根运动仍被连续速度替代 |
|
||||
| State transition order | PARTIAL | 输入状态、锁门/移动技能恢复、PlayerController、NetPlay 入口已定位;资源事件与网络事件的完整顺序未证实 |
|
||||
| Constants/units | PARTIAL | 角度/旋转阈值和 0/300/100ms 节流已记录;米/厘米、移动速度和目标距离不同 |
|
||||
| Timing/event sources | PARTIAL | 当前是 `_process` 预测 + signal;参考是 Actor motion event 驱动 |
|
||||
@@ -95,7 +100,16 @@
|
||||
- 用真实 `camera.heading()`、模型动作速度和网络 fake 记录同一方向序列的输入事件、移动目标、停止请求、动画状态和包时间线。
|
||||
- 补充死亡/传送/受击以及私店、情绪、移动技能和普通锁定的多门叠加顺序;各单门 held WASD 恢复已覆盖基础路径。
|
||||
- 继续对照 `.msa` 的 motion event、`AccumulationMovement` 根运动和当前 `get_move_motion_speeds` fallback,决定是否需要根运动适配层。
|
||||
- 核对相机旋转副作用与角色方向叠加,以及真实角色模型在 W/S 快切时根节点/骨骼姿态不翻转。
|
||||
- 真实角色模型在 W/S 快切时根节点/骨骼姿态不翻转的模型级测试仍缺。
|
||||
- **新增(2026-09-22 发现,未修复)**:`CPythonPlayer::__CanMove()` 还委托
|
||||
`CActorInstance::CanAct()`,后者在 `IsParalysis()/IsFaint()/IsSleep()` 时也拒绝移动
|
||||
(`ActorInstanceBattle.cpp` `CanAct`/`CanMove`)。当前端 `net_play.gd::_can_process_network_state()`
|
||||
只挡 `dead`/`stunned`/`knock_down`,没有 Paralysis/Faint/Sleep 三个状态的移动门。
|
||||
三者的触发源分别是萨满技能 `__Shaman_SetParalysis`(`InstanceBaseEffect.cpp:821`)、
|
||||
Python 侧 `SetFaint`(`PythonCharacterModule.cpp:1094/1101`)和效果可见性绑定的
|
||||
`SetSleep`(`InstanceBaseEffect.cpp:933`),当前端尚未定位这些触发源对应的服务端
|
||||
affect/状态位,需要先把三者映射到现有 affect 管线再决定移动门怎么接,属于比本轮
|
||||
camera 分支更大的独立子项,留给后续轮次。
|
||||
|
||||
### Audit round 2026-09-21
|
||||
|
||||
@@ -153,3 +167,58 @@
|
||||
- 继续对照 `NEW_SetMultiDirKeyState`、`NEW_MoveToDirection`、`AccumulationMovement` 和 `OnMove/OnMoving/OnWaiting/OnStop` 后,确认当前端仍是 Godot `_process` 连续速度预测,不能用输入测试替代 40250 `.msa` 根运动/动作事件源证据。
|
||||
|
||||
结论:本轮未修改实现;输入回归保持稳定,合同继续 `PARTIAL`。下一轮需要用真实动作资源和 fake network timeline 对拍固定 300cm 目标、根运动、停止事件和 MOVE/WAIT 包边沿。
|
||||
|
||||
### Implementation fix round 2026-09-22 — `movement.keyboard.motion/camera-auto-follow-rotation`
|
||||
|
||||
本轮处理的具体分支:40250 相机旋转副作用(此前 Remaining 与 Branch structure 行标注
|
||||
的 `没有实现同一套相机旋转副作用`)。
|
||||
|
||||
**参考实现**(`UserInterface/PythonPlayerInput.cpp:445-491` `CPythonPlayer::NEW_MoveToDirection`):
|
||||
`IsOpenPrivateShop()`/`isLock() && !IsUsingMovingSkill()` 提前返回(不跑相机分支)之后,
|
||||
若 `m_isCmrRot`(硬编码为 true,从未被关闭):
|
||||
把方向角 `fDirRot`(0=前 90=左 180=后 270=右,来自 `NEW_GetMultiKeyDirRotation` →
|
||||
`NEW_GetMouseDirRotation` → `GetDegreeFromPosition` 的屏幕空间角)折成有符号
|
||||
`fSigDirRot ∈ (-180,180]`,再折叠到 `fRotRat ∈ [-90,90]`(正前/正后为 0,正左/右为
|
||||
±90 最大速率,对角线线性减半),套公式
|
||||
`fRotDeg = -m_fCmrRotSpd(20.0) * fElapsedTime * fRotRat / 90`,通过 `CCamera::Roll(fRotDeg)`
|
||||
(D3D 增量式 roll,非绝对值)应用;这一步与是否已建立平移目标无关,且 `CPythonPlayer::Update`
|
||||
每帧对仍按住的方向键重试整条链路,所以只要方向键按住、且通过门检查,相机就持续转动。
|
||||
|
||||
**当前实现(修复前)**:`player_controller.gd::_keyboard_wish()` 只读 `camera.heading()`
|
||||
构造世界方向向量,从不写回 `camera.yaw`——完全没有这个副作用。
|
||||
|
||||
**修复**:新增 `PlayerController._camera_auto_rotate(dir, dt)`(`player_controller.gd`),
|
||||
在 `_process()` 的 `frozen/locked/private_shop_open/processing_emotion` 门之后、其余
|
||||
移动逻辑之前,用当前帧 `_wasd()` 的方向向量无条件调用(门未通过时函数整体在早退分支
|
||||
里不会被调用,等价于参考实现的 `IsOpenPrivateShop`/`isLock` 早退)。公式与参考实现的
|
||||
折算/折叠逻辑逐项相同(`fSigDirRot`/`fRotRat` 折叠、`CAMERA_AUTO_ROTATE_SPEED_DEG=20.0`
|
||||
对应 `m_fCmrRotSpd` 默认值、按 `dt` 与 `fRotRat/90` 线性缩放)。**平台适配**:D3D
|
||||
`CCamera::Roll` 的正方向与 Godot `yaw` 增大(视线转向左)手性相反,因此增量对 `fRotRat`
|
||||
取正号而非参考实现的负号;这一符号翻转用不变式核验,而不是逐位对照 D3D 数值——
|
||||
"持续按住某一方向时,相机朝该方向收敛(正前/正后不转,正左右转速最快,对角线减半),
|
||||
从不反向或越界",与参考实现的稳态行为一致。移动技能中的转向、以及触屏/摇杆
|
||||
`mobile_axis` 路径本轮未改动(后者是纯前端手势适配,参考实现没有对应的按键状态机,
|
||||
留作独立分支,不在本次改动范围内)。
|
||||
|
||||
**测试**:`keyboard_motion_timeline_test.gd` 新增六项断言(`GameCamera` 实例挂到
|
||||
`pc.camera`,不加入场景树,只读写 `yaw`):纯前/后不转、纯左/右等速反向、多帧速率恒定、
|
||||
对角线为纯左的一半速率、`locked` 门下按住方向键相机不转。修复前四项方向性断言必然
|
||||
失败(`cam.yaw` 恒为初始值 0),修复后全部通过。
|
||||
|
||||
**测试结果**:`keyboard_motion_timeline_test.gd`、`test_wasd_steering_parity.gd`、
|
||||
`test_no_auto_move_regression.gd`、`game_camera_test.gd`、`movement_parity_test.gd`、
|
||||
`player_move_test.gd`、`mouse_controller_test.gd`、`test_alignment_parity.gd` 修复后全部
|
||||
退出码 0(Godot 4.7.1 headless)。`player_motion_test.gd`(旧文件名,非本轮新增)在本环境
|
||||
用 `godot --headless --path project --script player_motion_test.gd` 单独运行时因脚本自身的
|
||||
`GDScriptNativeClass.clear_texture_cache()` 静态方法找不到而解析失败——本轮未改动此文件,
|
||||
`git log` 确认最后一次改动早于本会话;这是与本分支无关的预置环境问题,未修复,仅记录。
|
||||
`build-debug/` 的 `CMakeCache.txt` 记录的是另一个已废弃的检出路径
|
||||
(`/Users/shen/Work/Code/Metin2/...`),导致 `cmake --build build-debug` 在本环境直接报错,
|
||||
无法据此重跑 ctest/ASAN 套件;本轮改动只涉及 GDScript,未触及 `extension/` 下任何 C++,
|
||||
判断为与本分支无关的预置构建环境问题,同样未修复,仅记录供后续轮次或维护者决定是否
|
||||
重新配置构建目录。
|
||||
|
||||
**equivalence 更新**:Branch structure、Algorithms/formulas 两行的相机旋转子项由未闭合
|
||||
改为已闭合(见上表),`Remaining` 移除相机副作用项、新增 Paralysis/Faint/Sleep 移动门
|
||||
子项(见上)。合同整体仍为 `PARTIAL`——根运动/资源事件/同步受击门/Paralysis-Faint-Sleep
|
||||
仍未闭合。
|
||||
|
||||
Reference in New Issue
Block a user