I’m trying make an object in 3D space draggable along the X and Y axes, while locking its Z position. I want this drag and drop functionality no matter where the camera is in space, so I apply the basis of the camera to the velocity calculation to try to keep the movement in relation to the camera. So far, the code I have mostly works when the camera’s Y rotation is below 90 degrees, but above 0 the object has a circular movement effect, and beyond 90 the object starts to orbit around the mouse cursor.
The other issue I’m having with this code is keeping the object at a static depth. There is still a slight depth/Z axis translation when the mouse goes above and below the object’s ground position.
Behavior when camera Y rotation is close to 90 degrees:
Behavior when camera Y rotation is 0:
Desired Z-axis lock behavior:
Code:
extends CharacterBody3D
@export var drag_acceleration : float = 5.0
@export var stop_acceleration : float = 5.0
@export var drop_speed : float = 2.0
var is_draggable = false
var is_dragging = false
var camera : Camera3D
var camera_distance
func _ready():
camera = get_tree().current_scene.get_node("Camera3D")
camera_distance = global_position.distance_to(camera.global_position)
func _physics_process(delta: float) -> void:
var mouse_pos = get_viewport().get_mouse_position()
var target_position = camera.project_position(mouse_pos, camera_distance)
if is_dragging:
var raw_direction = global_position.direction_to(target_position)
var distance = global_position.distance_to(target_position)
var direction = (raw_direction +
camera.global_basis.z * raw_direction.z +
camera.global_basis.x * raw_direction.x).normalized()
velocity = direction * distance * drag_acceleration
else:
if !is_on_floor():
velocity = lerp(velocity, Vector3.ZERO, stop_acceleration * delta)
velocity += get_gravity() * delta * drop_speed
else:
velocity = lerp(velocity, Vector3.ZERO, stop_acceleration * delta)
move_and_slide()
func _unhandled_input(event: InputEvent):
if event is InputEventMouseButton and event.is_pressed() and is_draggable == true:
is_dragging = true
if event is InputEventMouseButton and !event.is_pressed():
is_dragging = false
func _on_mouse_entered() -> void:
is_draggable = true
Input.set_default_cursor_shape(Input.CURSOR_POINTING_HAND)
func _on_mouse_exited() -> void:
is_draggable = false
Input.set_default_cursor_shape(Input.CURSOR_ARROW)
Seems like this equation is the problem. Adding the direction to it’s x and z components (multiplied by other direction vectors) retains favoring axis-alignment and only partially reduces the undesired axis’ affect.
Using projection we can remove an axis’ influence, though I think your camera_distance variable needs updating when the object is closer or farther away.
# direction/distance isn't needed if it's to be combined i.e: direction * distance
var raw_difference: Vector3 = target_position - global_position
var planar_difference := raw_difference - raw_difference.project(-camera.global_basis.z)
velocity = planar_difference * drag_acceleration
## update `camera_distance` when clicked
func _unhandled_input(event: InputEvent):
if event is InputEventMouseButton and event.is_pressed() and is_draggable == true:
is_dragging = true
camera_distance = global_position.distance_to(camera.global_position)
Thank you for taking the time to answer this post!
I tried your code, and while it completely removed the issue of the object moving strangely when the camera became more rotated (woohoo!), dragging the object down will bring it closer to the camera (dragging it down enough eventually takes it out of view), and throwing it up seems to throw it away from the camera. Unfortunately I want the object to remain a fixed distance from the camera.
I’m not super familiar with vectors and projection and how this code exactly works, so I don’t really know where to start with adjusting it.
It is projecting the camera’s z forward direction. If your camera is tilted down, then moving it up will also move it “away” from the ground, moving down will drag the body against the ground, and because of collision “towards” the camera.
As an example I added a plane representing the camera’s z forward, moving the blue puck is restricted to this plane, notice it’s tilting off the ground? This is actually a fixed distance from the camera when dragging, I’m guessing you do not want a fixed distance, but instead a semi-fixed world position.
I think the best way to restrict this plane to be up-right may be to create a new basis without the camera’s rotation.x? Though the mouse will not line up with the object anymore.
var raw_difference: Vector3 = target_position - global_position
# probably don't need camera.rotation.z either as it's rare to roll the camera
var y_align_basis := Basis.from_euler(Vector3(0.0, camera.rotation.y, camera.rotation.z))
var planar_difference := raw_difference - raw_difference.project(-y_align_basis.z)
velocity = planar_difference * drag_acceleration
From here the target_position needs to be limited to the y_align_basis’s plane, but I’m having trouble finding a good way to find the distance from the camera ray/normal to a point on the plane.
var target_position := camera.project_position(mouse_pos, camera_distance_to_y_axis_plane)
Thank you for the explanation! That definitely makes much more sense to me now.
So the current target_position code causing the misalignment, hence needing to limit it? It seems like the closer the camera’s X rotation is to 0 the less misalignment occurs.