From c4e49cdc520cc72533d3e403e31e9af7d92a59fa Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 16 Aug 2026 16:39:52 -0400 Subject: [PATCH] fix: stabilize mouse camera dragging --- player/player.gd | 54 +++++++++++++++++++++++---- tests/camera_drag_validation.gd | 57 +++++++++++++++++++++++++++++ tests/camera_drag_validation.gd.uid | 1 + 3 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 tests/camera_drag_validation.gd create mode 100644 tests/camera_drag_validation.gd.uid diff --git a/player/player.gd b/player/player.gd index 6c90518..7dc9dc5 100644 --- a/player/player.gd +++ b/player/player.gd @@ -394,8 +394,11 @@ var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04) var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) var _camera_dragging: bool = false +var _camera_drag_button_held: bool = false var _camera_input_enabled: bool = true var _camera_drag_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE +var _camera_drag_capture_pending: bool = false +var _camera_drag_generation: int = 0 var _free_camera_active: bool = false var _free_camera_body: CharacterBody3D var _free_camera: Camera3D @@ -820,9 +823,6 @@ func _process(delta: float) -> void: ) return - if _camera_dragging and not Input.is_action_pressed("camera_drag"): - _set_camera_dragging(false) - if _is_camera_input_enabled(): var stick: Vector2 = _get_controller_camera_stick() if stick.length() > controller_camera_deadzone: @@ -1259,15 +1259,26 @@ func _input(event: InputEvent) -> void: return var mouse_button := event as InputEventMouseButton if mouse_button != null and event.is_action("camera_drag"): + _camera_drag_button_held = mouse_button.pressed _set_camera_dragging(mouse_button.pressed) return var mouse_motion := event as InputEventMouseMotion if mouse_motion == null or not _camera_dragging: return if _free_camera_active: - _rotate_free_camera(mouse_motion.relative * mouse_sensitivity) + _rotate_free_camera(mouse_motion.screen_relative * mouse_sensitivity) else: - _rotate_camera(mouse_motion.relative * mouse_sensitivity) + _rotate_camera(mouse_motion.screen_relative * mouse_sensitivity) + _request_camera_drag_capture() + + +func _notification(what: int) -> void: + # A captured pointer can miss its release when the application loses focus. + # Normal camera drags are ended by the matching mouse-button event; polling + # InputMap here is intentionally avoided because changing mouse mode can + # transiently clear the action and release an otherwise active drag. + if what == NOTIFICATION_APPLICATION_FOCUS_OUT and _camera_dragging: + _set_camera_dragging(false) func _unhandled_input(event: InputEvent) -> void: @@ -1321,11 +1332,38 @@ func _set_camera_dragging(active: bool) -> void: if _camera_dragging == active: return _camera_dragging = active + _camera_drag_generation += 1 + _camera_drag_capture_pending = false if active: _camera_drag_prior_mouse_mode = Input.mouse_mode - Input.mouse_mode = Input.MOUSE_MODE_CAPTURED - elif Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: - Input.mouse_mode = _camera_drag_prior_mouse_mode + else: + _camera_drag_button_held = false + if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + Input.mouse_mode = _camera_drag_prior_mouse_mode + + +func _request_camera_drag_capture() -> void: + if ( + _camera_drag_capture_pending + or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED + ): + return + _camera_drag_capture_pending = true + _capture_camera_drag_pointer.call_deferred(_camera_drag_generation) + + +func _capture_camera_drag_pointer(generation: int) -> void: + if generation != _camera_drag_generation: + return + _camera_drag_capture_pending = false + if ( + not _camera_dragging + or not local_control_enabled + or not _is_camera_input_enabled() + or not _camera_drag_button_held + ): + return + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func _get_network_aware_speed() -> float: diff --git a/tests/camera_drag_validation.gd b/tests/camera_drag_validation.gd new file mode 100644 index 0000000..a64f5ac --- /dev/null +++ b/tests/camera_drag_validation.gd @@ -0,0 +1,57 @@ +extends SceneTree + +const PlayerScene: PackedScene = preload("res://player/player.tscn") + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + root.size = Vector2i(1280, 720) + var player := PlayerScene.instantiate() as Player + root.add_child(player) + await process_frame + player.local_control_enabled = true + player.set_camera_input_enabled(true) + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + + var press := InputEventMouseButton.new() + press.button_index = MOUSE_BUTTON_RIGHT + press.button_mask = MOUSE_BUTTON_MASK_RIGHT + press.pressed = true + player.call("_input", press) + assert(bool(player.get("_camera_dragging"))) + if DisplayServer.get_name() != "headless": + assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE) + + var yaw := player.get_node("%CameraYaw") as Node3D + var previous_yaw: float = yaw.rotation.y + var motion := InputEventMouseMotion.new() + motion.button_mask = MOUSE_BUTTON_MASK_RIGHT + motion.relative = Vector2(18.0, -4.0) + motion.screen_relative = motion.relative + player.call("_input", motion) + assert(not is_equal_approx(yaw.rotation.y, previous_yaw)) + await process_frame + assert(bool(player.get("_camera_dragging"))) + if DisplayServer.get_name() != "headless": + assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED) + + var release := InputEventMouseButton.new() + release.button_index = MOUSE_BUTTON_RIGHT + release.pressed = false + player.call("_input", release) + assert(not bool(player.get("_camera_dragging"))) + if DisplayServer.get_name() != "headless": + assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE) + + player.call("_input", press) + assert(bool(player.get("_camera_dragging"))) + player.notification(NOTIFICATION_APPLICATION_FOCUS_OUT) + assert(not bool(player.get("_camera_dragging"))) + + player.queue_free() + await process_frame + print("Camera drag validation: PASS") + quit() diff --git a/tests/camera_drag_validation.gd.uid b/tests/camera_drag_validation.gd.uid new file mode 100644 index 0000000..a37fad9 --- /dev/null +++ b/tests/camera_drag_validation.gd.uid @@ -0,0 +1 @@ +uid://dku3kyarg3fxu