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
+67
View File
@@ -3,6 +3,7 @@
extends SceneTree
const PlayerController = preload("res://player_controller.gd")
const GameCamera = preload("res://game_camera.gd")
var _fail := 0
@@ -129,9 +130,75 @@ func _init() -> void:
"held direction resumes after emotion ends")
pc._input(_key(KEY_W, false))
pc._input(_key(KEY_W, false))
# 40250 CPythonPlayer::NEW_MoveToDirection m_isCmrRot: holding a pure
# forward/backward direction must not roll the camera (fRotRat == 0 at the
# axis), while pure left/right rolls it fastest, and diagonals roll at
# half rate. See player_controller.gd::_camera_auto_rotate.
pc.stop()
var cam := GameCamera.new()
pc.camera = cam
cam.yaw = 0.0
pc._unhandled_input(_key(KEY_W, true))
pc._process(0.1)
_ck(is_zero_approx(cam.yaw), "holding pure forward does not roll the camera")
pc._input(_key(KEY_W, false))
cam.yaw = 0.0
pc._unhandled_input(_key(KEY_S, true))
pc._process(0.1)
_ck(is_zero_approx(cam.yaw), "holding pure backward does not roll the camera")
pc._input(_key(KEY_S, false))
cam.yaw = 0.0
pc._unhandled_input(_key(KEY_A, true))
var yaw_after_one := 0.0
for _i in range(5):
pc._process(0.1)
if _i == 0:
yaw_after_one = cam.yaw
_ck(yaw_after_one > 0.001, "holding left starts rolling the camera immediately")
_ck(is_equal_approx(yaw_after_one * 5.0, cam.yaw), "camera roll rate is constant while held")
var left_yaw := cam.yaw
pc._input(_key(KEY_A, false))
cam.yaw = 0.0
pc._unhandled_input(_key(KEY_D, true))
pc._process(0.1)
pc._process(0.1)
pc._process(0.1)
pc._process(0.1)
pc._process(0.1)
_ck(is_equal_approx(cam.yaw, -left_yaw),
"holding right rolls the camera the same rate opposite left")
pc._input(_key(KEY_D, false))
cam.yaw = 0.0
pc._unhandled_input(_key(KEY_W, true))
pc._unhandled_input(_key(KEY_A, true))
for _i in range(5):
pc._process(0.1)
_ck(is_equal_approx(cam.yaw, left_yaw * 0.5),
"a forward-left diagonal rolls the camera at half the pure-left rate")
pc._input(_key(KEY_A, false))
pc._input(_key(KEY_W, false))
# The camera-roll side effect is gated exactly like translation: no roll
# while the actor cannot move.
cam.yaw = 0.0
pc.stop()
pc.locked = true
pc._unhandled_input(_key(KEY_A, true))
pc._process(0.2)
_ck(is_zero_approx(cam.yaw), "locked movement gate also suppresses the camera roll")
pc.locked = false
pc._input(_key(KEY_A, false))
pc.stop()
player.free()
pc.free()
cam.free()
if _fail == 0:
print("PASS: keyboard_motion_timeline_test")
quit(0)