3D Character Controller Directions (without cam)

Godot Version

4.3 Stable

Question

What is the simplest way to create more than 8 directions for a Gamepad Controller ?

Hi, I am new in the coding world, I am currently developing a 3D platformer and I try to improve my 3D character controller. Especially for use it with the left stick of a gamepad. I think there is something missing in my code, something around angles but I did not find the answer.

So I am asking you please, how can I do my movement direction not just follows 8 lines/vectors?

To be clear : I want to move my character not only at 45° in diagonal direction, when I turn my stick I would like to take each possible angle directions between 0° and 45°, if my stick points between this two values.

I worked around lerp and clamp but nothing conclusive… Naively I think that the movement direction must be set by a variable angle between the 8 directions. And I do not know how to do.

In advance, I am sorry if this is confusing, coding is still a bit confusing for me, but I want to learn more.

You will find my script bellow and sorry but as a new member of this forum I can not upload a video to put my words into pictures :

extends CharacterBody3D

@onready var animation = $AnimationPlayer

@export var speed = 5
@export var jump_impulse = 20

@export var acceleration = 2
@export var fall_acceleration = 75


func _physics_process(delta):
	
	var direction = Vector3.ZERO
	
	if Input.is_action_pressed("right"):
		direction.x += 1.0
	if Input.is_action_pressed("left"):
		direction.x -= 1.0
	if Input.is_action_pressed("down"):
		direction.z += 1.0
	if Input.is_action_pressed("up"):
		direction.z -= 1.0
	
	# Move and rotate the character in direction.
	if is_on_floor() and direction != Vector3.ZERO:
		direction = direction.normalized()
		rotation.y = lerp_angle(rotation.y, atan2(-direction.z, direction.x), delta * 10)
		if Input.is_action_pressed("speed"):
			animation.play("Run")
		else:
			animation.play("Walk")
		
	else:
		animation.play("Idle")

	# Ground velocity.
	if Input.is_action_pressed("speed"):
		velocity.x = direction.x * speed * acceleration
		velocity.z = direction.z * speed * acceleration
	else:
		velocity.x = direction.x * speed
		velocity.z = direction.z * speed

	# Jumping.
	if is_on_floor() and Input.is_action_just_pressed("jump"):
		velocity.y = jump_impulse

	# Air velocity.
	if not is_on_floor():
		velocity.y = velocity.y - (fall_acceleration * delta)


	move_and_slide()

You can replace your if-else block checking the directions by Input.get_vector(). This accommodates for both, digital keyboard and analog controller input.

1 Like

You mean under #Move and rotate the character in direction block? I tried but same issue. If you can tell me more details I will appreciate that so much because I do not know really how to do what you say… Sorry I do not have all the logic!

This should be replaced with get_vector, then converting to a vector3

func _physics_process(delta):
	var direction: Vector2 = Input.get_vector("left", "right", "up", "down")
	var direction3 := Vector3(direction.x, 0, direction.y)

normalized is ruining your controller support, it set the direction vector length to always be 0.0 or 1.0, no in-between half-tilt movement is possible. It’s also unnecessary when using Input.get_vector. You can use the Vector2.angle instead of atan2 as well. lerp and it’s derivitives should not be used with delta it actually makes it more frame-dependant, the third parameter represents a percentage between a and b which changes every frame.

The rest of your script looks great!

Here’s the changes I’d recommend all together

func _physics_process(delta: float) -> void:
	var direction: Vector2 = Input.get_vector("left", "right", "up", "down")
	var direction3 := Vector3(direction.x, 0, direction.y)
	
	# Move and rotate the character in direction.
	if is_on_floor() and direction != Vector2.ZERO:
		rotation.y = lerp_angle(rotation.y, direction.angle(), 0.1)
		if Input.is_action_pressed("speed"):
			animation.play("Run")
		else:
			animation.play("Walk")
	else:
		animation.play("Idle")

	# Ground velocity.
	if Input.is_action_pressed("speed"):
		velocity.x = direction3.x * speed * acceleration
		velocity.z = direction3.z * speed * acceleration
	else:
		velocity.x = direction3.x * speed
		velocity.z = direction3.z * speed

	# Jumping.
	if is_on_floor() and Input.is_action_just_pressed("jump"):
		velocity.y = jump_impulse

	# Air velocity.
	if not is_on_floor():
		velocity.y = velocity.y - (fall_acceleration * delta)

	move_and_slide()

If you are still recieving only 8-directional inputs then your controller may be gated rather than using potentiometers, like a arcade fight stick.

3 Likes

Thank you so much! It works but now my rotation on the Z axis is inverted. When I move up my character turn back and look at me, inversely if I move down he look at the top.

Edit: when I change the order in the get_vector() entry that change the direction so I switched “up” and “down” but nothing to do for looking rotation

You can flip the get_vector actions for “up” and “down” if an axis is inverted. The angle could be different from what you expect if your player’s visual isn’t facing the forward -z axis.

I did it, I switched “up” and “down” but nothing to do for looking rotation, any idea. Thanks a lot for your time!!

EDIT: fixed, I just apply negative symbol to the direction.y in the var direction3

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.