Camera movement with a game pad

Godot Version

4.4.1

Question

Hey folks, I’m trying to implement a free look camera on a game controller using the right analogue stick to rotate the camera around a flying spaceship in 360 degrees so the player can look anywhere. The left stick controls the movement of the ship.

I already have this working very nicely with a mouse but I’d like to include controller support as well. I’m using the PhantomCamera3D add-on which helps immensely with the camera movement but I need help with passing it the correct values to smoothly rotate around the ship.

The code I have written sorta works but there a few problems with it which I’ll list below:

		if Input.is_action_pressed("free_look_up"):
			free_look_rotation_x -= deg_to_rad(1)
		elif Input.is_action_pressed("free_look_down"):
			free_look_rotation_x += deg_to_rad(1)
		elif Input.is_action_pressed("free_look_right"):
			free_look_rotation_y -= deg_to_rad(1)
		elif Input.is_action_pressed("free_look_left"):
			free_look_rotation_y += deg_to_rad(1)
		
		pcam_rotation_degrees.x = free_look_rotation_x
		pcam_rotation_degrees.y = free_look_rotation_y
		
		var camera_rotation = pcam.get_third_person_quaternion()
		var target_rotation = Quaternion.from_euler(pcam_rotation_degrees)
		
		camera_rotation = camera_rotation.slerp(target_rotation, 0.9)
		# camera_rotation = camera_rotation * target_rotation
		
		pcam.set_third_person_quaternion(camera_rotation)

The issue I have are:

  1. The movement is very stuttery and inconsistent
  2. The rotation goes off-axis very easily and goes all wibbly-wobbly unless you’re super gentle with the stick.
  3. Pushing the stick one way should keep the rotation rate consistently moving on that axis but what happens is it only registers an input when you start pushing it. If you move it to the edge of the ring it just stops moving altogether. Is this hitting the deadzone or something?

I tried using slerp() to interpolate the rotation which also kinda worked but was still a worse result than the above code.

What changes would you make to my code to help with the above issues?

Is this sample in a _process function or is in under _input? if the latter then it will only trigger rotation when a input is pressed, not over time.

Ah I see! It is in the _input() function yes. I changed it and it works far better, thanks!

I have the mouse look functionality in an _unhandled_input() so I figured the joypad functionality should go in _input() for some reason.

The movement of the camera is still a bit janky and will randomly jump to a different axis but the movement is completely smooth now. I reckon I’ll just need to play around with how I get the input from the controller to fix that.

Yeah, not too sure what causes the random jumps, maybe the slerp isn’t what you need but it’s tough to say for sure.


I would make sure your units are correct, seems like you are taking radians and applying them to “rotation_degrees”, luckily this variable is being used as radians in Quaternion.from_euler.

free_look_rotation_y += deg_to_rad(1)
		
pcam_rotation_degrees.x = free_look_rotation_x
pcam_rotation_degrees.y = free_look_rotation_y
		
var target_rotation = Quaternion.from_euler(pcam_rotation_degrees)

But if used in _process this will be 1° per frame which will mean slower movement on slower computers and faster on high-framerate computers. Use * delta and a higher value to make this per second

# rotates 60 degrees per second
free_look_rotation_y += deg_to_rad(60) * delta

The mouse movement makes sense in a _input() function because the mouse’s movement is accumulated, event.screen_relative tells how many dots the mouse moved since last _input() where as += 1° does not tell how far or how long the controller has been held, and process helps to solve the latter, acting as long as the control is held.

1 Like

Yeah the degrees part is just leftover from a previous approach but I found out using radians and quats yields better results.

I will try multiplying the rotation with delta and report back. Perhaps the framerate dependency is causing some weirdness. Hopefully that’s all it is.

1 Like

So I got it much much closer. Pretty psyched about this approach. It’s still juddery and sharp but it’s pretty damn close to what I wanted. I want the speed to ramp a bit more rather than just GO. I know I need something like lerp but I’m unclear which values to lerp to which others.

Down the line I’d like to have a way to reset the camera to the XZ plane of the world because even though it’s space with no up or down it’s still a little jarring for me as a player when it goes off that plane.

For those curious here is my 90%-of-the-way code:

func _look_around() -> void:
		
		var rotation_rate: Vector2
	# We want to change the rotation in predictable steps
	# while the joystick is pushed in a certain direction rather than using
	# the discrete values sent by the controller.
		if Input.is_action_pressed("free_look_up"):
			# free_look_rotation_x = Vector2.DOWN
			rotation_rate.x = -Input.get_action_strength("free_look_up")
			
		elif Input.is_action_pressed("free_look_down"):
			# free_look_rotation_x = Vector2.UP
			rotation_rate.x = Input.get_action_strength("free_look_down")
			
		elif Input.is_action_pressed("free_look_right"):
			# free_look_rotation_y = Vector2.RIGHT
			rotation_rate.y = -Input.get_action_strength("free_look_right")
			
		elif Input.is_action_pressed("free_look_left"):
			# free_look_rotation_y = Vector2.LEFT
			rotation_rate.y = Input.get_action_strength("free_look_left")
		
		pcam_rotation_vector.x = deg_to_rad(remap(rotation_rate.x, -1, 1, -5, 5)) 
		pcam_rotation_vector.y = deg_to_rad(remap(rotation_rate.y, -1, 1, -5, 5)) 
		pcam_rotation_degrees.z = 0.0
		
		var camera_rotation = pcam.get_third_person_quaternion()
		var target_rotation = Quaternion.from_euler(pcam_rotation_vector)
		
		camera_rotation = camera_rotation * target_rotation
		
		pcam.set_third_person_quaternion(camera_rotation)

Small advice you can reduce all of your if/action_strengths by using Input.get_vector, and your remap is functionaly equivalent to multiplying by 5. I don’t see delta in the equation anymore either so it may be frame dependent again.

func _look_around() -> void:
	var rotation_rate: Vector2 = Input.get_vector("free_look_right", "free_look_left", "free_look_up", "free_look_down")
	pcam_rotation_vector = Vector3(rotation_rate.x, rotation_rate.y, 0) * 5
	
	var camera_rotation = pcam.get_third_person_quaternion()
	var target_rotation = Quaternion.from_euler(pcam_rotation_vector)
	
	camera_rotation = camera_rotation * target_rotation
	
	pcam.set_third_person_quaternion(camera_rotation)

I am not familiar with phantom camera but maybe using the set_third_person_rotation instead would be better?

var accumulated_rotation := Vector3.ZERO

func _process(delta: float) -> void:
	var rotation_rate: Vector2 = Input.get_vector("free_look_right", "free_look_left", "free_look_up", "free_look_down")
	var pcam_rotation_vector := Vector3(rotation_rate.x, rotation_rate.y, 0) * 100 * delta

	accumulated_rotation += pcam_rotation_vector

	var old_rotation := pcam.get_third_person_rotation()
	var new_rotation := old_rotation.move_toward(accumulated_rotation, 80 * delta)
	
	pcam.set_third_person_rotation(new_rotation)

Thanks, I may well try this. The quaternion method does make the camera go in unexpected directions.

I tried it with delta and it didn’t really work but could have been a me problem I’m not sure.