I got you.
Short answer is you need to stack the inputs from non-mouse sources from frame-to-frame, whereas from a mouse the input is relative from frame-to-frame, and you need to reset the inputs (or get new ones). So here’s a solution that allows the player to use either type of input for camera rotation at any time, and you can get the bits you need for your rotation. (I thought you would appreciate the deep dive.)
This is what I use for the right stick on a controller to look. (Bear with me.)
#gamepad.gd
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventJoypadMotion:
if event.axis == JOY_AXIS_RIGHT_X:
Controller.look = Vector2(-event.axis_value * horizontal_look_sensitivity, Controller.look.y)
if event.axis == JOY_AXIS_RIGHT_Y:
Controller.look = Vector2(Controller.look.x, -event.axis_value * vertical_look_sensitivity)
This is what I use for the to look. (Just for completeness and to see how they can both work together.)
#mouse.gd
func _unhandled_input(event: InputEvent) -> void:
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
if event is InputEventMouseMotion:
Controller.look = -event.relative * sensitivity
This is the class that tracks the look
value, and also the last input used. (Overkill if you’re not trying to map mouse movement and keyboard input or joystick input, but still.
#controller.gd
## Stores the amount of movement in the x/y direction that the player is trying
## to look in a 3D game.
var look := Vector2.ZERO
## Stores the last input used by the player for UI interaction hint updates.
## Private so that it cannot be modified extrenally. This is a read-only value
## outside this singleton. (Or would be if GDScript enforced that.)
var _last_input_type := LastInput.KEYBOARD_AND_MOUSE
## Updates the last input type for use throughout the game.
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton or event is InputEventMouseMotion or event is InputEventKey:
_last_input_type = LastInput.KEYBOARD_AND_MOUSE
elif event is InputEventJoypadButton or event is InputEventJoypadMotion:
_last_input_type = LastInput.GAMEPAD
## Returns the last input type used by the player.
func get_last_input_type() -> LastInput:
return _last_input_type
This is the code I use to translate those inputs into rotation for the camera, but the pivots are just Node3D nodes.:
func _physics_process(delta: float) -> void:
update_rotation()
func update_rotation() -> void:
horizontal_pivot.rotate_y(Controller.look.x)
vertical_pivot.rotate_x(Controller.look.y)
vertical_pivot.rotation.x = clampf(vertical_pivot.rotation.x,
deg_to_rad(upwards_rotation_limit),
deg_to_rad(downwards_rotation_limit)
)
apply_rotation()
######This is the important bit if your input is coming from a mouse you have to reset the input each frame because the values are relative - however from any other input they're not - so you have to retain them to build up consistency in your rotation.
if Controller.get_last_input_type() == Controller.LastInput.KEYBOARD_AND_MOUSE:
Controller.look = Vector2.ZERO
func apply_rotation() -> void:
spring_arm_3d.rotation.y = horizontal_pivot.rotation.y
camera_3d.rotation.x = vertical_pivot.rotation.x
Obviously you only need the rotation code, but I thought it might be helpful to see how mouse rotation is different from gamepad/keyboard rotation.
The full controller code is here as an addon if you want to dig into it. GitHub - dragonforge-dev/dragonforge-controller: A Controller autoload singleton to handle game input from gamepads (controllers) and keyboard/mouse. The Camera control stuff isn’t up yet - still working on that addon, but the code works.
I developed this code while doing a course from gamedev.tv where they only had mouse input, and I wanted controller input too. I also wanted the player to be able to switch back and forth. Then I decided I only wanted to write this code one more time so I’ve been working on plugins.
Hope that helps.