Godot 4.0: Block raycast when clicking on UI

zhifei
3 min readMay 28, 2023

Say I have a feature where I want to spawn an object into the scene at where my mouse clicked on. But I only want this feature to be enabled when a UI button is clicked on, as well as to disable that feature.

The technical task I need to do is as follows:

  1. Click on a button to allow raycast function
  2. When raycast function is allowed to call, click on the screen to shoot a raycast from mouse click position
  3. When the raycast detected something, spawn the object
  4. Click on the same button to disallow raycast function

Raycast

To call the raycast function, we can do the following:

func _input(event: InputEvent):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
var from = camera.project_ray_origin(event.position)
var to = from + camera.project_ray_normal(event.position) * 1000.0

var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(from, to)
var result = space_state.intersect_ray(query)
# Handle result here

Seems simple enough, and if there’s a UI on top of the screen, it should be able to block the mouse interaction from happening. Except it doesn’t.

One suggestion commonly found on the internet, is to handle the mouse event in _unhandled_input function instead of _input. However, the problem is that raycast function doesn’t run at all in _unhandled_input.

The solution? Store the raycast result and handle it in _process.

New class

To tackle the solution, we will first need to create a new class to store the raycast result:

class RaycastResult:
var position: Vector2

Why do we need a new class? It’s more of a personal preference really. Because when handling it in _process, I want to check whether a RaycastResult actually exists, before I proceed to do anything with it.

var raycast_result: RaycastResult = null

...

func _process(delta: float):
if raycast_result != null
# Handle raycast result here
_handle_raycast_result()
raycast_result = null

Notice how we immediately set raycast_result to null as soon as we check whether it exist? This is to ensure that whatever function we call — _handle_raycast_result() — to handle the result, is only called ONCE.

Handle raycast result in _process

Next, we can update the initial _input function to _unhandled_input, as this method will prevent anything within it from being called when a UI is pressed on:

func _unhandled_input(event: InputEvent):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
var next_raycast_result = RaycastResult.new()
next_raycast_result.position = event.position
raycast_result = next_raycast_result

Then in _handle_raycast_result, which is the function that’s called in _process to handle raycast result, we will handle the raycast function calling:

func _handle_raycast_result():
var from = camera.project_ray_origin(event.position)
var to = from + camera.project_ray_normal(event.position) * 1000.0

var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(from, to)
var result = space_state.intersect_ray(query)
# Handle raycast result, spawn object or something

Further note

If nothing happened when you click the screen, no raycast (nor the print functon you put in to test the raycast) is called, chances are, your root UI that’s covering the entire screen has its mouse’s “Filter” settings set to “Stop”. You just need to set it as “Pass”, to make sure the part of the screen without an actual UI or button doesn’t block your click event.

Also, please make sure your button’s mouse setting has its “Filter” option set to “Stop” though, because we don’t want the mouse event to pass through the button.

--

--

zhifei

Software engineer. Jack of all stacks, master of none.