I'm new to Godot and I've been struggling with this issue for quite some time now:

In this prototype, the player is controlling a crosshair on screen by moving their mouse:

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseMotion:
		camera_look_input = event.global_position

func _process(delta: float) -> void:
	process_input()

func process_input():
	# Mouse
	if Global.control_mode == Global.DEBUG_CONTROL_MOUSE:
		raw_input_dir = camera_look_input

func move_crosshair():
	crosshair.position = raw_input_dir
	crosshair.position.x = clamp(crosshair.position.x, 0 ,get_viewport().get_visible_rect().size.x)
	crosshair.position.y = clamp(crosshair.position.y, 0, get_viewport().get_visible_rect().size.y)

Then, a CharacterBody3D goes towards that crosshair:

func get_target_position() -> Vector2:
        var target_position : Vector2
	target_position.x = crosshair.position.x
	target_position.y = crosshair.position.y
	
	return target_position

func get_distance_to(point : Vector2) -> float:
	var distance = point.distance_to(camera.unproject_position(global_position))
	distance = min(distance, max_crosshair_distance)
	distance = max(min_crosshair_distance, distance)
	return distance / max_crosshair_distance

All of the calculations are done on a script on the CharacterBody3D:

func _physics_process(delta: float) -> void:
	# Get the input direction and handle the movement/deceleration.
	move_crosshair()
	var target_position = get_target_position()

        # Get the direction from the crosshair to the ship
	var crosshair_direction = target_position.direction_to(camera.unproject_position(global_position))
	var distance_accel = get_distance_to(target_position)

	var crosshair_vector = Vector3(crosshair_direction.x, crosshair_direction.y, 0.0)
        # Calculate the ship's direction based on its current transformation
	var ship_direction = (global_transform.basis * crosshair_vector).normalized()
        # Set maximum speed in both axis
	var target_velocity = Vector3(ship_direction.x * pitch_speed, ship_direction.y * yaw_speed, 0.0)

        # Gradual acceleration
	current_velocity.x = lerp(current_velocity.x, target_velocity.x * distance_accel, movement_acceleration * delta)
	current_velocity.y = lerp(current_velocity.y, target_velocity.y * distance_accel, movement_acceleration * delta)

	velocity = current_velocity
	move_and_slide()

All of this works well when the CharacterBody3D is facing the same direction as the world coordinates. I can even move forward automatically by adding a constant velocity.z.

Now, my idea was to be able to design levels by using Path3D and PathFollow3D, and still keep all the functionality described above. Think of it as a XY plane on a rail.

This is my setup:

  • I've got a pair of RemoteTranform3D that will follow the path. As far as I know, placing the CharacterBody3D as a child of PathFollow3D would make it stick to the path, and I need it to move freely on the XY plane. The same idea applies to the camera, as I might want to move slightly following the player to make the scene more dynamic;
  • Both CharacterBody3D and Camera3D as nested inside a Node3D. The camera has an offset on the Z axis so that it's behind the player automatically.

And this is the small test I've performed:

The green arrows represents which way the path aligns with the world's coordinate, and so everything works flawlessly. On the red arrow sections, the x-axis input translates to the world's x-axis instead of the desired one. Both the CharacterBody3D, as well as the Camera3D, align correctly at the new orientation, but my operations result in a Vector3 which z-axis is not parallel with the current basis.

I have been searching online and educating myself on vector math, including the official documentation, but I'm clearly missing something. Thank you for your attention, and apologies if the issue is something trivial that I'm not seeing.

  • xyz replied to this.

    WorkingHard Do the movement in 3d. Instead of unprojecting everything to 2d, project everything into 3d space and do the calculations there.

      xyz Thank you for your response. I've refactor my code in order to perform calculations in 3D space:

      func _physics_process(delta: float) -> void:
              # Move the 2D crosshair on screen
      	move_crosshair()
              var target_position = get_target_position()
      	# Project the 2D target into 3D space. That z-axis projection is a close approximation, verified by placing a Mesh3D
      	crosshair_position.global_transform.origin = camera.project_position(target_position, 10)
      	# Fix the new 3D target basis (I've noticed that the projection keeps the world's basis)
      	crosshair_position.global_transform.basis = global_transform.basis
      	# Calculate vector towards the 3D target
      	var crosshair_direction = global_transform.origin.direction_to(crosshair_position.global_transform.origin).normalized()
      	# Calculate distance towards the 3D target
      	var distance_accel = get_distance_to(crosshair_position.global_transform.origin)
      	# Apply speed factors
      	var target_velocity = Vector3(crosshair_direction.x * pitch_speed, crosshair_direction.y * yaw_speed, 0.0)
      		
      	# Calculate velocity
      	current_velocity.x = lerp(current_velocity.x, target_velocity.x * distance_accel, movement_acceleration * delta)
      	current_velocity.y = lerp(current_velocity.y, target_velocity.y * distance_accel, movement_acceleration * delta)
      	
              # Progress in the path
      	path_follow.progress += 10 * delta
      		
      	# Apply velocity
      	velocity = current_velocity
      		
      	move_and_slide()

      Unfortunately, the same issue as before still happens:

      • When the ship is on a segment of the path that is parallel to the world's axis, crosshair_direction works as expected;
      • When the ship is on a segment of the path that is perpendicular to the world's axis, crosshair_direction is still parallel to the world's axis. I've verified this claim by printing its value: on this path's segment, moving the 2D cursor on the x-axis translates to changes to crosshair_direction.z.
      • xyz replied to this.

        WorkingHard Your description of the problem is not really clear. Try to make the simplest possible example that demonstrates the problem.

          xyz Thank you for your patience. Let's see if I can make myself clearer with some screenshots of the scene.

          I've modified the scene so that:

          • We are looking at it from a new camera from a top perspective;
          • The pink prism represents the position and rotation of the player's camera;
          • The blue ball represents the 3D projection of the 2D crosshair, based on the player's camera;
          • In the upper-left corner, I'm displaying the value of crosshair_direction from my calculations on the previous post.


          In this first screenshot, the player is moving parallel to the world's coordinates. The blue ball follows the position of the crosshair. The value of crosshair_direction.x is also parallel with the player's x-axis, and so the ships moves towards the blue ball on their local x-axis.


          In this second screenshot, the player is moving perpendicular to the world's coordinates. The blue ball moves correctly to the same position as the crosshair (relative to the player's camera point of view). However, crosshair_direction is still based on the world's coordinates, and so the change is reflected in crosshair_direction.z. Therefore, the ship doesn't move towards the blue ball on their local x-axis.

          • xyz replied to this.

            WorkingHard Looks like you're overcomplicating things. If you know player position and blue ball position, and you want player to move towards the blue ball then the velocity direction is simply ball_position-player_position. Normalize this vector, multiply it by speed and that's your velocity.

              xyz Correct me if I'm wrong, but that is what I'm already doing in my code:

              var crosshair_direction = global_transform.origin.direction_to(crosshair_position.global_transform.origin)

              As per Godot documentation, direction_to:

              Returns the normalized vector pointing from this vector to to. This is equivalent to using (b - a).normalized().

              Then, with the resulting direction:

              var target_velocity = Vector3(crosshair_direction.x * pitch_speed, crosshair_direction.y * yaw_speed, 0.0)

              However, I finally found my issue. I was so fixated in rotating vectors, when I only had to stop ignoring the z-axis. So, my new code includes:

              var target_velocity = Vector3(crosshair_direction.x * pitch_speed, crosshair_direction.y * yaw_speed, crosshair_direction.z * pitch_speed)
              
              current_velocity.z = lerp(current_velocity.z, target_velocity.z * distance_accel, movement_acceleration * delta)

              That way, I'm applying the pitch movement of the ship both on the x and z-axis depending on the current orientation. Now the movement is just perfect. Thank you for your time!