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:
shenlei
2026-09-22 16:17:00 +09:00
co-authored by Claude Sonnet 5
parent c93894313a
commit 7df97e01f9
7 changed files with 354 additions and 139 deletions
+29
View File
@@ -38,6 +38,7 @@ const SPEED_RUN := 4.8
const KEYBOARD_MOVE_TARGET_M := 3.0 # 40250 NEW_MoveToDirection: Dst = Cur + 300 px
const RUN_HOLD_KEY := KEY_SHIFT
const ARRIVE_EPS := 0.05
const CAMERA_AUTO_ROTATE_SPEED_DEG := 20.0 # 40250 CPythonPlayer::m_fCmrRotSpd 默认值
const PICK_RADIUS := 1.4 # 点选命中半径(米)
# __IsMovableGroundDistance:点地目标离脚下太近就不动(避免原地抖)。参考默认由
# player.SetMovableGroundDistance 从 game.py 设置;此处用参考缺省值(~1 m)。§3.1
@@ -503,6 +504,29 @@ func _wasd() -> Vector2:
d.x = 1.0
return d.normalized()
# 对齐 40250 CPythonPlayer::NEW_MoveToDirection 的 m_isCmrRot 分支:dir 是 _wasd() 的相机相对
# 输入向量(0,-1=正前)。折算成角色相对相机的方位角 fDirRot0=前 90=左 180=后 270=右,
# 与 GetDegreeFromPosition 的取值一致),再按参考实现同样的公式折叠到 [-90,90] 的
# fRotRat:正前/正后为 0(不转),正左/右为 ±90(转速最快),对角线为 ±45(减半)。
# 参考实现用 CCamera::Roll(fRotDeg)D3D 增量式 rollfRotDeg=-speed*dt*fRotRat/90);
# Godot 的 yaw 增大对应视线转向左,和 D3D 的 Roll 正方向手性相反,因此这里的增量对
# fRotRat 取正号而不是参考的负号 —— 这是platform_adaptation,用“相机持续转向新朝向、
# 从不越过目标”这一收敛不变式核验(keyboard_motion_timeline_test.gd),而不是逐位对照
# D3D 数值。
func _camera_auto_rotate(dir: Vector2, dt: float) -> void:
if camera == null or not camera.has_method("heading") or dir == Vector2.ZERO:
return
var dir_rot_deg := fposmod(rad_to_deg(atan2(-dir.x, -dir.y)), 360.0)
var sig_dir_rot := dir_rot_deg
if sig_dir_rot > 180.0:
sig_dir_rot -= 360.0
var rot_rat := sig_dir_rot
if rot_rat > 90.0:
rot_rat = 180.0 - rot_rat
elif rot_rat < -90.0:
rot_rat = -180.0 - rot_rat
camera.yaw += deg_to_rad(CAMERA_AUTO_ROTATE_SPEED_DEG * dt * rot_rat / 90.0)
func _blocked(x: float, z: float) -> bool:
return world != null and world.has_method("is_blocked") and bool(world.call("is_blocked", x, z))
@@ -595,6 +619,11 @@ func _process(dt: float) -> void:
if world and world.has_method("sample_height") and is_instance_valid(player):
player.position.y = float(world.call("sample_height", player.position.x, player.position.z))
return
# 40250 CPythonPlayer::Update 每帧对仍按住的方向键重试 NEW_SetMultiDirKeyState ->
# NEW_MoveToDirection,其 m_isCmrRot 分支与是否已建立平移目标无关,门检查(上面
# 的 frozen/locked/shop/emotion)通过后即执行,因此放在移动逻辑之前、无条件按
# 当前方向键调用。
_camera_auto_rotate(_wasd(), dt)
if force_walk:
_run = false
elif force_run: