Weird RigidBody Movement While Following Mouse

Godot Version

4.3 Stable

Question

I am working on a game with controls where you click to pick up and move objects around a space by moving the mouse until you click again to place them.

I previously had my set-up as such script wise for the detection of objects, it is attached to the only static camera:

Controls.gd v1

extends Camera3D

var mouse_position_3d;
var mouse_position_2d;
var projection_3d;
var viewport; 
var object_to_hold;
var marker; # Whatever is held at the time is the marker
var object_held; # Whether or not the marker is currently attached.
var object_looked_at; # Whether or not any object is being looked at. 
var last_highlighted; # The previously highlighted tile.
const STARTING_POSITION = Vector3(0, 2, -6);
const STARTING_ROTATION = Vector3(0, -160, 0);



# Called when the node enters the scene tree for the first time.
func _ready():
	object_held = false;
	viewport = get_viewport();
	last_highlighted = null;
	
func _on_button_pressed() -> void:
	object_held = false;
	add_child(marker); # Replace with function body.

# we could decouple this by including this in each object (clickable.gd) then emit a signal to catch here.
func _input(event):
	if event is InputEventMouse:
		mouse_position_2d = viewport.get_mouse_position();
		transform_screen_to_world_coordinates(mouse_position_2d);
	
	#if event is InputEventMouseButton:
		#if event.button_index == MOUSE_BUTTON_LEFT:
			#handle_marker_state();
	
# Projects mouse coordinates from screen to 3D world.
func transform_screen_to_world_coordinates(mouse_position):
	var origin := project_ray_origin(mouse_position);
	var direction := project_ray_normal(mouse_position);
	var ray_length := 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);
	
	check_for_collisions(query);

# Detects if objects are in the mouses path.
func check_for_collisions(query):
	var collision = get_world_3d().direct_space_state.intersect_ray(query);
	
	if(last_highlighted != null):
		last_highlighted.emit_signal("unhighlight");
	
	if(!collision.values().is_empty()):
		var collider = collision.values()[4];
		
		if collider.name == "RigidBody3D":
			# marker = collider; # cpmmented out because it may be causing an issue with the marker always being attached
			object_looked_at = true;
		
		if collider.name.contains("Terrain_"):
			collider.emit_signal("highlight")
			last_highlighted = collider;
			
		if object_held:
			move_marker(collision)

func move_marker(result):
	var surface_normal = result.get("normal");
	mouse_position_3d = result.get("position"); 
	marker.global_position = mouse_position_3d  # Set the position directly
	marker.look_at(mouse_position_3d + surface_normal, Vector3.UP)
	marker.rotate_object_local(Vector3.RIGHT, -marker.rotation.x) # Cancel X rotation

# After getting the signal with the object itself, attach it to the camera.
func receive_object_as_marker(object) -> void:
	marker = object
	object_to_hold = object;
	object_held = true;
	#add_child(marker); # Replace with function body.

# Should leave the marker in place where it is.
func dispatch_object_as_marker() -> void:
	marker = null;
	object_to_hold = null;
	object_held = false;

# The logic here is that we only want to grab an object that is being "looked at"
func _on_grabbed(object):
	if object_looked_at:
		receive_object_as_marker(object)
		print("picking up");

# We only want to let go of an object that is held"
func _on_let_go(object):
	if object_held:
		dispatch_object_as_marker()
		print("letting go");
	else:
		print("No Object Held"); # Should never print lmao 

# Probably will remove. Currently unused. Keep in case we need to modify the game object at runtime.
func rebuild_marker():
	marker = object_to_hold;

# Need to carefully look at.
func handle_marker_state():
	if !object_held:
		marker = null;
	if object_held:
		marker = object_to_hold;

This script below was attached to any object that could be picked up, and sent signals to the script above.

# This script gets attached to any game item or element that is able to be clicked on by the user.
extends Node

signal grabbed(object);
signal let_go(object);

var is_grabbed;

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	is_grabbed = false;
	print("made")
	if get_parent().get_parent().get_parent() != null:	
		grabbed.connect(get_tree().get_first_node_in_group("camera")._on_grabbed);
		let_go.connect(get_tree().get_first_node_in_group("camera")._on_let_go);

# Handles being picked up and let go.
func _input(event: InputEvent) -> void:
		if event is InputEventMouseButton:
			if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
				if is_grabbed:
					let_go.emit(self);
					is_grabbed = false;
				elif !is_grabbed:
					grabbed.emit(self);
					is_grabbed = true;

The movement was nice, except for some rapid movement you can see in the video below. I also noticed you could click anywhere, and it would “pick up” the object.

So I decided to put the logic for whether or not objects were picked up in their own script, and that is where I got the issue. The problem of registering being hovered over and picking up only if so was solved, but the movement all of the sudden went so bad.

I am especially confused because, although I made plenty of changes, I don’t think I messed with the function for movement itself, so I would have assumed it would work fine regardless of where it was called. Here is what it looks like:

And here is my current script which gets attached to grabbable objects, which I’m in the process of trying to fix by introducing signals again in the _ready() function just before posting this to see if I can get the behavior happening again in the

Clickable.gd v2

extends Node


var is_grabbed = false
var is_looked_at = false
var viewport
var mouse_position_2d
var mouse_position_3d
var camera
var current_collider: RigidBody3D = null  # Explicitly type the collider

# These both tell the Controls script about each action happening.
signal grabbed;
signal let_go;

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	viewport = get_viewport()
	camera = get_tree().get_first_node_in_group("camera")

	# Weird way to get to the camera since this script does not appear in the tree. Suggestions for improvement
	if get_parent().get_parent().get_parent() != null:	
		var camera = get_tree().get_first_node_in_group("camera")
		grabbed.connect(get_tree().get_first_node_in_group("camera")._on_grabbed);
		let_go.connect(get_tree().get_first_node_in_group("camera")._on_let_go);
		print("connected")

# Handles input events.
func _input(event: InputEvent) -> void:
	if event is InputEventMouse:
		mouse_position_2d = viewport.get_mouse_position()
		
		if is_grabbed:
			 #Only update movement when grabbed, don't check for new collisions
			transform_screen_to_world_coordinates(mouse_position_2d, true)
		else:
			#Check for collisions when not grabbed
			transform_screen_to_world_coordinates(mouse_position_2d, false)

	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
			if is_grabbed:
				# Release the object
					
				is_grabbed = false
				if current_collider:
					current_collider.freeze = false  # Re-enable physics
					current_collider = null
	
			elif !is_grabbed and is_looked_at and current_collider:
				# Grab the object
				is_grabbed = true
				current_collider.freeze = true  # Disable physics
				grabbed.emit()
				
	
	if event.is_action_pressed("Rotate left") and is_grabbed:
		rotate_object(90);
		print("rl");

# Transforms screen coordinates to world coordinates.
func transform_screen_to_world_coordinates(mouse_position, move_object: bool):
	var origin = camera.project_ray_origin(mouse_position)
	var direction = camera.project_ray_normal(mouse_position)
	var end = origin + direction * camera.far
	var query := PhysicsRayQueryParameters3D.create(origin, end)
	var result = camera.get_world_3d().direct_space_state.intersect_ray(query)
	
	if move_object:
		move_with_mouse(result)
	else:
		check_for_collisions(result)  # Only check collisions when not moving

# Detects if objects are in the mouse's path.
func check_for_collisions(result):
	if result and result.has("collider"):
		var collider = result.get("collider")
		if collider is RigidBody3D:
			is_looked_at = true
			current_collider = collider  # Store the collider
			print("Looked at")
			#show_label()
			return
	# Reset if no valid collider
	is_looked_at = false
	current_collider = null
	print("Not Looked at")
	#hide_label();

# Moves the object with the mouse using look_at and rotate_object_local.
func move_with_mouse(result):
	if not result or not current_collider:
		return  # Exit if invalid
	
	var surface_normal = result.get("normal")
	mouse_position_3d = result.get("position")
	
	# Update position and apply Y offset
	current_collider.global_position = mouse_position_3d
	current_collider.global_position.y += 1
	
	# Align with surface normal and adjust rotation
	current_collider.look_at(mouse_position_3d + surface_normal, Vector3.UP)
	current_collider.rotate_object_local(Vector3.RIGHT, -current_collider.rotation.x)

func _unhandled_input(event: InputEvent) -> void:
	if is_grabbed and current_collider:
		handle_rotation_input(event)

func handle_rotation_input(event: InputEvent) -> void:
	var rotation_amount := 90.0  # Degrees
	var direction := 0
	
	if event.is_action_pressed("rotate left"):  # Q key
		direction = -1
	elif event.is_action_pressed("rotate right"):  # E key
		direction = 1
	
	if direction != 0:
		rotate_object(rotation_amount * direction)

func rotate_object(degrees: float) -> void:
	# Rotate around the object's local Y-axis (up/down)
	current_collider.rotate_object_local(Vector3.UP, deg_to_rad(degrees))
	
	# Alternative: Rotate around global Y-axis
	# current_collider.rotate_y(deg_to_rad(degrees))

func show_label():
	pass;
	
func hide_label():
	pass;

and finally, the camera script which may also have an issue:

extends Camera3D

var mouse_position_3d;
var mouse_position_2d;
var projection_3d;
var viewport; 
var object_to_hold;
var marker; # Whatever is held at the time is the marker
var object_held; # Whether or not the marker is currently attached.
var object_looked_at; # Whether or not any object is being looked at. 
var last_highlighted; # The previously highlighted tile.
const STARTING_POSITION = Vector3(0, 2, -6);
const STARTING_ROTATION = Vector3(0, -160, 0);



# Called when the node enters the scene tree for the first time.
func _ready():
	object_held = false;
	viewport = get_viewport();
	last_highlighted = null;
\
# Handles mouse movement.
func _input(event):
	if event is InputEventMouse:
		mouse_position_2d = viewport.get_mouse_position();
		transform_screen_to_world_coordinates(mouse_position_2d);
	
# Projects mouse coordinates from screen to 3D world.
func transform_screen_to_world_coordinates(mouse_position):
	var origin := project_ray_origin(mouse_position);
	var direction := project_ray_normal(mouse_position);
	var end := origin + direction * far;
	var query := PhysicsRayQueryParameters3D.create(origin, end);
	var result := get_world_3d().direct_space_state.intersect_ray(query);
	
	check_for_collisions(query);

# Detects if objects are in the mouses path.
func check_for_collisions(query):
	var collision = get_world_3d().direct_space_state.intersect_ray(query);
	
	if(last_highlighted != null):
		last_highlighted.emit_signal("unhighlight");
	
	if(!collision.values().is_empty()):
		var collider = collision.values()[4];
		
		if collider.name.contains("Terrain_"):
			collider.emit_signal("highlight")
			last_highlighted = collider;

func move_marker(result):
	var surface_normal = result.get("normal");
	mouse_position_3d = result.get("position"); 
	marker.global_position = mouse_position_3d  # Set the position directly
	marker.global_position.y += 1;
	marker.look_at(mouse_position_3d + surface_normal, Vector3.UP)
	marker.rotate_object_local(Vector3.RIGHT, -marker.rotation.x) # Cancel X rotation

# After getting the signal with the object itself, attach it to the camera.
func receive_object_as_marker(object) -> void:
	marker = object
	object_to_hold = object;
	object_held = true;
	add_child(marker); # Replace with function body.

# Should leave the marker in place where it is.
func dispatch_object_as_marker() -> void:
	object_held = false;

# Only called by clickable objects.
func _on_grabbed(object):
	receive_object_as_marker(object)
	print("picking up");

# Only called by picked-up objects.
func _on_let_go(object):
	dispatch_object_as_marker()
	print("letting go");

Just to recap, the movement used to work fine, but the logic was bad for detecting when something should be considered picked up. So I fixed that by moving the logic out into a script for grabbable objects (called Clickable.gd). Even though I never changed the code for movement itself, this caused the movement to get very laggy…

old movement function taken from Controls.gd v1

var surface_normal = result.get("normal");
	mouse_position_3d = result.get("position"); 
	marker.global_position = mouse_position_3d  # Set the position directly
	marker.global_position.y += 1;
	marker.look_at(mouse_position_3d + surface_normal, Vector3.UP)
	marker.rotate_object_local(Vector3.RIGHT, -marker.rotation.x) # Cancel X rotation

old movement function taken from Clickable.gd v2

if not result or not current_collider:
		return  # Exit if invalid
	
	var surface_normal = result.get("normal")
	mouse_position_3d = result.get("position")
	
	# Update position and apply Y offset
	current_collider.global_position = mouse_position_3d
	current_collider.global_position.y += 1
	
	# Align with surface normal and adjust rotation
	current_collider.look_at(mouse_position_3d + surface_normal, Vector3.UP)
	current_collider.rotate_object_local(Vector3.RIGHT, -current_collider.rotation.x)

Thank you!!