Best way to handle raycasting?

In my game no signal, I do a lot of raycasting to find out what the user has clicked on. Generally, my hierarchies look something like this:

ClickableObject
├ ... # Some arbitrary level of nesting
│   └ StaticBody3D
│     └ CollisionShape3D
└ OtherStuff

Since all of the logic for what I want to do is on the ClickableObject, I’ve gotten into this pattern of recursively searching upwards for the thing that should handle the click, which looks something like this:

@onready var camera: Camera3D = $Camera3D

func parent_where(node: Node, filter) -> Node:
    if node == null:
        return null
    elif filter.call(node):
        return node
    else:
        return parent_where(node.get_parent(), filter)

func do_raycast() -> void:
    var mouse_pos := get_mouse_position()
    var space_state := get_world_3d().direct_space_state

    var origin := camera.project_ray_origin(mouse_pos)
    var end := origin + camera.project_ray_normal(mouse_pos) * 100
    var query := PhysicsRayQueryParameters3D.create(origin, end)

    var raycast := space_state.intersect_ray(query)
    if raycast.has("collider"):
        var collider: Node = raycast.collider
        var clicked_object := parent_where(
            collider, func(a: Node) -> bool: return a is ClickableObject
        )

        # Do something with clicked_object

There are some problems with this approach, but I’m not worried about performance here. Instead, I’ve run into situations where this results in the wrong behavior. For example, it might look up the tree and happen to find a parent that is clickable, but I don’t actually want to click on it. So, that leads to me having to rearrange how everything is parented.


Lately, I’ve been thinking that there must be a better way. Something I’ve been considering is to extend StaticBody3D, like this:

class_name ClickableStaticBody3D
extends StaticBody3D

signal clicked_on() # Emit this signal from the raycasting code.

Another is to connect to the mouse_entered and mouse_exited signals on the StaticBody3D to the ClickableObject, but the docs suggest that these signals are not reliable.

I don’t find either solutions particularly satisfying, so I’m curious how y’all do it.

1 Like

I used the mouse signals and didn’t find any bad stuff happening, except, it won’t work well if you happen to have another body in between the camera and the object you wanna touch (let’s say an invisible wall).

You then have to play with mask and stuff.

I happen to have removed it for my purpose.

I think emitting the signal from the static body is the thing that is the most “Godot” way.

Some people would have this on the static body:

@export stuff_i_need_to_click: ClickableObject

func raycast_touched_me():
    stuff_i_need_to_click.click()

I like it less personally.

1 Like

The way I’ve implemented it in Re:Placement is basically the “extending the CollisionObject” approach. (Godot 3 code follows)

A Clickable object in my case is an Area.

class_name Clickable
extends Area

signal hovered()
signal unhovered()
signal pressed()

export var enabled: bool = true setget set_enabled

onready var shape: CollisionShape = $CollisionShape

func _ready() -> void:
	shape.disabled = !enabled

func hover() -> void:
	emit_signal("hovered")

func unhover() -> void:
	emit_signal("unhovered")

func press() -> void:
	emit_signal("pressed")

func set_enabled(v: bool) -> void:
	enabled = v
	if shape:
		shape.disabled = !v

These come into contact with a RayCast node, which updates automatically every physics frame. I’m using a FPP camera, so the raycast always points at what’s in the middle of the screen (under the crosshair).

extends RayCast

signal pressed(target)

var target: Clickable = null
var frozen: = false

func _process(delta: float) -> void:
	if frozen:
		return

	if Input.is_action_just_pressed("use") && is_instance_valid(target) && target.enabled:
		target.press()
		emit_signal("pressed", target)

func _physics_process(delta: float) -> void:
	if frozen:
		return

	var collider: = get_collider() as Clickable
	if collider != target && !Input.is_action_pressed("use"):
		if is_instance_valid(target):
			target.unhover()
		if collider:
			collider.hover()
		target = collider

As you can see, I basically created my own version of mouse_entered and mouse_exited. I keep track of what the raycast is touching, but don’t update it while player is holding Mouse1.

Because of the mess my interaction code is, the raycast also emits a pressed signal, as a shortcut to have the player controller do something with the Clickable.

1 Like