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!!