How do I convert player input direction into player model rotation?

Godot Version

4.4.1

Question

Alright, It’s been a while since I’ve asked a question here. So I’ll be a little rusty with my explanations.

I’m currently working on the movement and general game feel of the player. One of the major aspects is the player rotating in the input direction of movement (when not shooting) regardless of camera position. This is what I’m having trouble with.

My codebase is quite the brain-burner due to it’s multiplayer implementation, so I’ll only share the relevant bits of code.

How movement input is gathered: (Only the relevant parts)

# Get player input vectors
	var input:Vector2 = Input.get_vector("move_right", "move_left", "move_backward", "move_forward")
			
	# Move the player on the client
	Client_Player.start_moving(input, delta)

Function for player movement: (Only the relevant parts)

func start_moving(input:Vector2, delta:float) -> void:
	
	# Set the current movement direction
	Global_Movement_Direction = Global_Rotation_Pivot.basis \
	* Vector3(input.x, 0, input.y).normalized()
	
	# Movement force
	velocity.x = Global_Movement_Direction.x * Current_Movement_Speed
	velocity.z = Global_Movement_Direction.z * Current_Movement_Speed
	
	# Failed attempt at rotating the player (DOESN'T WORK)
	Body_Parts.global_rotation.y = input.y

Player’s node tree:

Note: The ‘GlobalRotationPivot’ is the thing that rotates with the camera. The body parts (AKA player model) are separate.

So, with all this in mind, how would I convert the player’s input direction into rotation for the player’s body?

Also, completely unrelated to the question, writing this made my brain so foggy. It’s because I haven’t really dug into my codebase in a while. I’m just going through a readjustment period. That’s all.

  • Demetrius Dixon

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.

1 Like

@dragonforge-dev
Ok, all this extra made me quite confused.

I have no knowledge about programming controller input, so that’s lost on me.

I’m trying to convert WASD movement into Y axis rotation for the player model. The WASD rotation’s forward/back/left/right ARE relative to the player’s camera direction.

Apologies if this sounds rude, I’m just rusty with GDScript.

You can use input.angle() for rotation around the y axis, you may have to modify some components for it to be perfect, such as flipping a input axis or adding a quarter rotation.

Body_Parts.global_rotation.y = input.angle()

# modifications I've had to do before
Body_Parts.global_rotation.y = Vector2(-input.x, input.y).angle() + PI/2

You should not normalize Input.get_vector, it breaks controller support.

1 Like

No offense taken. I gave you too much and didn’t leave a simplified answer.

func _unhandled_input(event: InputEvent) -> void:
	var input: Vector2 = Input.get_vector("move_right", "move_left", "move_backward", "move_forward")

	Body_Parts.rotate_y(input.x)

The main problem you’re having is you have to rotate the y-axis with x inputs, and vice-versa.

1 Like

@gertkeno
Could you clarify which line the normalize is in? I don’t understand what you mean by input.get_vector.

And I’m curious on how that breaks controller support. I don’t doubt you at all, but wouldn’t controller input be a separate function or built into the same one? I haven’t gotten to that point yet, so I have no frame of reference.

Is it this one?

Global_Movement_Direction = Global_Rotation_Pivot.basis \
* Vector3(input.x, 0, input.y).normalized()

Yes that normalized() breaks controller support, you can tilt a joystick half way to get half an input, but a normalized vector is always exactly 1 length or full strength. It would be better to use limit_length, but Input.get_vector already limits it’s returned value to 1 length, so you will not get square or extra fast diagonal movement.

Here is where you use Input.get_vector, then send it to the start_moving function.

1 Like

That’s very good to know. Thank you.