I do not know how to make a character move like in Diablo 4 or TorchLight 2

Godot Version

v4.1.3

Question

Hello, I want to move a character like in Diablo 4 or TorchLight 2, but only teleportation works (if I want to change something, it either doesn’t work or gives an error). Tell me what to do.

Working code:

extends CharacterBody3D

func _physics_process(delta):

	if Input.is_action_just_pressed("mouse_left"):
		var viewport := get_viewport()
		var mouse_position := viewport.get_mouse_position()
		var camera := viewport.get_camera_3d()
		var origin := camera.project_ray_origin(mouse_position)
		var direction := camera.project_ray_normal(mouse_position)
		var ray_length := camera.far
		var end := origin + direction * ray_length
		var space_state := get_world_3d().direct_space_state
		var query := PhysicsRayQueryParameters3D.create(origin, end)
		var result := space_state.intersect_ray(query)

		var mouse_position_3D:Vector3 = result["position"]
		position += mouse_position_3D - position
		position.y = 0

	move_and_slide()

Try moving towards the mouse, you successfully get the difference in position now it needs to be normalized to a speed. Finally using velocity instead of position directly.

extends CharacterBody3D

const SPEED = 1

func _physics_process(delta):

	if Input.is_action_just_pressed("mouse_left"):
		var viewport := get_viewport()
		var mouse_position := viewport.get_mouse_position()
		var camera := viewport.get_camera_3d()
		var origin := camera.project_ray_origin(mouse_position)
		var direction := camera.project_ray_normal(mouse_position)
		var ray_length := camera.far
		var end := origin + direction * ray_length
		var space_state := get_world_3d().direct_space_state
		var query := PhysicsRayQueryParameters3D.create(origin, end)
		var result := space_state.intersect_ray(query)

		var mouse_position_3D:Vector3 = result["position"]
		var direction := mouse_position_3D - position
		velocity = direction.normalized() * SPEED 
		velocity.y = 0

	move_and_slide()
1 Like

Thanks! And you can tell me how to make sure that when the specified point is reached with the mouse, the character stops. Thank you in advance.

That wil be a distance check, continuing from the last code we can use the direction again. A vector can be broken down into two components, the direction and length. We got the direction by normalizing and we can get the length with aptly named .length().

var mouse_position_3D:Vector3 = result["position"]
var direction := mouse_position_3D - position
if direction.length() < 0.1:
	velocity = Vector3.ZERO
else:
	velocity = direction.normalized() * SPEED 

However the underlying math for length() is quite expensive, especially for scripts that happen each frame. It’s the Pythagorean theorem! The big expense here is a square root.

func length():
	return sqrt(x*x + y*y)

Godot, Unity, Unreal, and many many more game engines recognize this and add a sqrt free version. Then we can compare squared numbers instead. It’s a small code change but could be 2x faster.

const min_character_distance = 0.1 ** 2 # squared

var mouse_position_3D:Vector3 = result["position"]
var direction := mouse_position_3D - position
if direction.length_squared() < min_character_distance:
	velocity = Vector3.ZERO
else:
	velocity = direction.normalized() * SPEED 
2 Likes

I’m probably just dumb, but if this code is supposed to look like this:

extends CharacterBody3D

const SPEED = 1
const min_character_distance = 0.1 ** 2 # squared

func _physics_process(delta):

	if Input.is_action_just_pressed("mouse_left"):
		var viewport := get_viewport()
		var mouse_position := viewport.get_mouse_position()
		var camera := viewport.get_camera_3d()
		var origin := camera.project_ray_origin(mouse_position)
		var direction := camera.project_ray_normal(mouse_position)
		var ray_length := camera.far
		var end := origin + direction * ray_length
		var space_state := get_world_3d().direct_space_state
		var query := PhysicsRayQueryParameters3D.create(origin, end)
		var result := space_state.intersect_ray(query)

		var mouse_position_3D:Vector3 = result["position"]
		direction = mouse_position_3D - position

		if direction.length_squared() < min_character_distance:
			velocity = Vector3.ZERO
		else:
			velocity = direction.normalized() * SPEED

		velocity.y = 0

	move_and_slide()

Then, won’t the check be performed only when “mouse_left” is triggered. And it turns out that the check does not pass, and the character goes into the void (I have not completed the map yet).

And I apologize in advance for such a possibly stupid question. :frowning:

Ah I though it it was a hold click to move, that’s how I play Torchlight 2; you are absolutely correct!

You would have to store the target position in a variable and continously do this distance check, or calculate how long it takes to reach that position and set velocity to zero once complete, minding to interrupt if another click is placed. I would recommend the first since it makes hold-to-move easier to program in addition to click-to-move

1 Like

Thank you!

I know I’ve finished the discussion, but I still want to share with you the working code where the character moves smoothly to the click position. Thanks gertkeno, it wouldn’t have happened without you!

Code:

extends CharacterBody3D

const SPEED = 1
var target_position = null

func _physics_process(delta):

	if Input.is_action_just_pressed("mouse_left"):
		var viewport := get_viewport()
		var mouse_position := viewport.get_mouse_position()
		var camera := viewport.get_camera_3d()
		var origin := camera.project_ray_origin(mouse_position)
		var direction := camera.project_ray_normal(mouse_position)
		var ray_length := camera.far
		var end := origin + direction * ray_length
		var space_state := get_world_3d().direct_space_state
		var query := PhysicsRayQueryParameters3D.create(origin, end)
		var result := space_state.intersect_ray(query)

		target_position = result["position"]

	if target_position != null:
		var direction = target_position - position
		velocity = direction.normalized() * SPEED
		velocity.y = 0

		if position.distance_to(target_position) <= 0.1:
			target_position = null
			velocity.y = 0
			velocity.x = 0
			velocity.z = 0

	move_and_slide()

Thanks for showing the final code, I always appreciate that!

There is also a distance_squared_to() function that implements that optimization I wrote about.

1 Like