How can I separate my movement direction and velocity from my Camera3D's X rotation axis?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By WalterBennet

Hello, I’m making a 3D first person game and I’ve been struggling with character movement.

If I’m looking downward or upward with my camera while moving, the movement direction and velocity are shifted toward unwanted values. The X axis’s value of the camera is clamped between -1.5 and 1.5. Basically, I’d like the movement direction and velocity to be independent of whether I’m looking up or down.

Hopefully someone has some kind of clue of what might be happening. I’ve linked the methods involved down below. If anything else is required, let me know so I can provide more information.

Hope you all have a good day, cheer!

func _rotate_camera(delta: float, sens_mod: float = 1.0) -> void:
	look_dir += Input.get_vector("look_left", "look_right", "look_up", "look_down")
	camera.rotation.z = lerp(camera.rotation.z, (-move_dir.x * (rotation_amount if (enable_camera_z_rotation && !leaning) else 0.0)), delta * 10)
	body.rotation.y -= look_dir.x * camera_sensitivity * sens_mod * delta
	camera.rotation.x = clamp(camera.rotation.x - look_dir.y * camera_sensitivity * sens_mod * delta, -1.5, 1.5)
	look_dir = Vector2.ZERO

func _walk(delta: float) -> Vector3:
	move_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_backwards")
	var _forward: Vector3 = (camera.transform.basis * body.transform.basis) * Vector3(move_dir.x, 0, move_dir.y)
	var walk_dir: Vector3 = Vector3(_forward.x, 0, _forward.z).normalized()
	walk_vel = walk_vel.move_toward(walk_dir * (walking_speed + (running_speed if running else 0.0)) * move_dir.length(), acceleration * delta)
	return walk_vel

What do you mean by “unwanted values”?

TRAILtheGREAT | 2023-04-29 16:24

Well, simply put, it moves the player into incorrect direction. For example, if I’m pressing the forward key while looking either down or up, the player will be moved either left or right depending on the value of the camera’s Y rotation axis.

It’s somewhat tricky to describe without actually testing it, so I’ve linked the whole code and the scene if you wish to take a close look or even test it yourself.


Character controller (player.gd)

class_name Player extends CharacterBody3D 

@export_category("Player Variables")
@export_range(1, 35, 1) var walking_speed: float = 4 # m/s
@export_range(1, 35, 1) var running_speed: float = 3 # m/s
@export_range(10, 400, 1) var acceleration: float = 40 # m/s^2
@export_range(0.1, 3.0, 0.1) var jump_height: float = 1 # m

@export_category("Player Controls")
@export var always_run: bool = false
@export var enable_camera_z_rotation: bool = true

@export_category("Camera Variables")
@export_range(0.1, 9.25, 0.05, "or_greater") var camera_sensitivity: float = 6.2
@export_range(75, 120, 1) var camera_fov: float = 90
@export_range(0, 0.1, 0.01) var rotation_amount: float = 0.06
@export_range(0.40, 1.7, 0.05) var camera_crouched_height: float = 0.85

var walking: bool = false
var jumping: bool = false
var running: bool = false
var leaning: bool = false
var crouching: bool = false
var crouch_toggle: bool = false
var mouse_captured: bool = false

var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")

var move_dir: Vector2 # Input direction for movement
var look_dir: Vector2 # Input direction for look/aim

var walk_vel: Vector3 # Walking velocity 
var grav_vel: Vector3 # Gravity velocity 
var jump_vel: Vector3 # Jumping velocity

var base_position_x: float
var base_position_y: float

@onready var body: CharacterBody3D = self
@onready var camera: Camera3D = $Camera

func _ready() -> void:
	capture_mouse()

func _input(event: InputEvent) -> void:
	running = always_run != Input.is_action_pressed("run")
	leaning = Input.is_action_pressed("lean_left") or Input.is_action_pressed("lean_right")
	if event is InputEventMouseMotion: look_dir = event.relative * 0.01
	if crouch_toggle:
		if Input.is_action_just_released("crouch"):
			crouching = false
			crouch_toggle = false
	if Input.is_action_just_pressed("toggle_crouch"):
		crouch_toggle = !crouch_toggle
	crouching = crouch_toggle or Input.is_action_pressed("crouch")
	if Input.is_action_just_pressed("jump"): jumping = true
	if Input.is_action_just_pressed("exit"):
		capture_mouse() if (Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE) else release_mouse()

func _physics_process(delta: float) -> void:
	if mouse_captured: _rotate_camera(delta)
	velocity = _walk(delta) + _gravity(delta) + _jump(delta)
	walking = true if velocity != Vector3.ZERO else false
	_running(delta)
	_leaning(delta)
	_crouching(delta)
	move_and_slide()

func capture_mouse() -> void:
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	mouse_captured = true

func release_mouse() -> void:
	Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
	mouse_captured = false

func _rotate_camera(delta: float, sens_mod: float = 1.0) -> void:
	look_dir += Input.get_vector("look_left", "look_right", "look_up", "look_down")
	camera.rotation.z = lerp(camera.rotation.z, (-move_dir.x * (rotation_amount if (enable_camera_z_rotation && !leaning) else 0.0)), delta * 10)
	body.rotation.y -= look_dir.x * camera_sensitivity * sens_mod * delta
	camera.rotation.x = clamp(camera.rotation.x - look_dir.y * camera_sensitivity * sens_mod * delta, -1.5, 1.5)
	look_dir = Vector2.ZERO

func _walk(delta: float) -> Vector3:
	move_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_backwards")
	var _forward: Vector3 = (camera.transform.basis * body.transform.basis) * Vector3(move_dir.x, 0, move_dir.y)
	var walk_dir: Vector3 = Vector3(_forward.x, 0, _forward.z).normalized()
	walk_vel = walk_vel.move_toward(walk_dir * (walking_speed + (running_speed if running else 0.0)) * move_dir.length(), acceleration * delta)
	return walk_vel

func _gravity(delta: float) -> Vector3:
	grav_vel = Vector3.ZERO if is_on_floor() else grav_vel.move_toward(Vector3(0, velocity.y - gravity, 0), gravity * delta)
	return grav_vel

func _jump(delta: float) -> Vector3:
	if jumping:
		if is_on_floor(): jump_vel = Vector3(0, sqrt(4 * jump_height * gravity), 0)
		jumping = false
		return jump_vel
	jump_vel = Vector3.ZERO if is_on_floor() else jump_vel.move_toward(Vector3.ZERO, gravity * delta)
	return jump_vel

func _running(delta: float) -> void:
	camera.fov = lerp(camera.fov, camera_fov + 15, delta * 10) if running else lerp(camera.fov, camera_fov, delta * 10)
	if running:
		crouch_toggle = false
		crouching = false

func _crouching(delta: float) -> void:
	camera.position.y = lerp(camera.position.y, camera_crouched_height, delta * 10) if (crouching && !running) else lerp(camera.position.y, 1.7, delta * 10)
	
	if crouching:
		running = false

func _leaning(delta: float) -> void:
	if leaning:
		camera.rotation.z = clamp(lerp(camera.rotation.z, 0.7 if Input.is_action_pressed("lean_left") else -0.7, delta * 3), -0.25, 0.25)
		camera.position.x = clamp(lerp(camera.position.x, -0.30 if Input.is_action_pressed("lean_left") else 0.30, delta * 6), base_position_x - 1.0, base_position_x + 1.0)
	else:
		camera.position.x = lerp(camera.position.x, base_position_x, delta * 4)
		if camera.position.x == base_position_x: base_position_x = camera.position.x

**Player scene (player.tscn)**
[gd_scene load_steps=3 format=3 uid="uid://cvq4dvoyms4vw"]

[ext_resource type="Script" path="res://player/player.gd" id="1_2f8j2"]

[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_3rsb5"]
radius = 0.45
height = 1.8

[node name="Player" type="CharacterBody3D"]
script = ExtResource("1_2f8j2")

[node name="CShape" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
shape = SubResource("CapsuleShape3D_3rsb5")

[node name="Camera" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, 0)
fov = 90.0

WalterBennet | 2023-04-29 16:51

:bust_in_silhouette: Reply From: TRAILtheGREAT

I think this might be your problem:

var _forward: Vector3 = (camera.transform.basis * body.transform.basis) * Vector3(move_dir.x, 0, move_dir.y)

You’re multiplying by the camera’s basis, which is rotating your forward vector unnecessarily. The camera is only rotated vertically and you don’t want the vertical direction of the camera to effect movement.

Well, you’re correct. I swear I thought I had removed it earlier, as multiplying the camera’s basis with the body’s basis was an old fix that always felt weird that it “worked”. Guess I thought wrong! Thanks a lot mate, cheers!

WalterBennet | 2023-04-29 18:18