Clicking on top collider only

Godot Version

4.2.1

Intro

Hi, I’m new to Godot and trying to learn. I’m enjoying the engine so far and thin it has a lot to offer but I’m having some difficulty adjusting. I have found many other posts asking similar things but all with different answers, none of which quite seem to address the particular issue I’m having, and to be honest (with no disrespect to other’s solutions) they all feel like a bodge rather than a proper solution. I have tried to incorporate some of the ideas in similar posts but I’m still having no joy.

Question

What is the best way to have a click interaction work with only the top game object in a scene? I have tried this using a button element and it sort of worked but I have no way to control the shape - it’s always just a rectangle - so would like to get this to work on a 2D node so I can couple the interaction to a collider shape.

Setup

I have:

  • A 2d node called main, basically just serves as a root for me to put other things in
  • A StaticBody2D with a sprite and a collider - we’ll call this the stage
  • A StaticBody2D with a sprite and a collider - we’ll call this item

The stage is added to the main scene and has a script attached so that when the user clicks on the stage (inside the collider) it instantiates a new item at that position (have tried both making the new item a child of the stage and tried making the new item a child of the main scene). It also adds the new item to an array for referencing later. The item scene is dropped into the inspector through an exported variable.

The item has a script attached to allow drag and click interactions - the drag interaction moves the item, the click operation for the moment just prints something to the console.

The problem

When interacting with the items on the stage this also procs the click interaction on the stage spawning a new item. A new item should only be instantiated when the click isn’t interacting with one of the items already present.

Code

Script on stage:

extends StaticBody2D

@export var item : PackedScene
var items : Array

func _unhandled_input(event):
#func _on_input_event(viewport, event, shape_idx):
#func _input(event):
	if event is InputEventMouseButton and event.is_pressed():
		#print(event)
		var new_item = item.instantiate()
		new_item.global_position = event.position - position
		items.append(new_item)
		add_child(new_item)

As you can see I have tried different input handler function calls.

Script on items

extends StaticBody2D

var button_down : bool = false
var dragging : bool = false
var drag_distance = 2
var mouse_start : Vector2 = Vector2(0,0)
var pos_delta : Vector2 = Vector2(0,0)

func _physics_process(delta):
	if dragging:
		drag()

func drag():
	global_position = get_global_mouse_position() + pos_delta
		
func _on_input_event(viewport, event, shape_idx):
	get_parent().get_viewport().set_input_as_handled()
	get_viewport().set_input_as_handled()
	if event is InputEventMouseButton:
		if event.is_pressed():
			button_down = true
			pos_delta = global_position - get_global_mouse_position()
			mouse_start = get_global_mouse_position()
		if event.is_released():
			if not dragging:
				print("click")
			button_down = false
			dragging = false
	if event is InputEventMouseMotion:
		if button_down:
			if get_global_mouse_position().distance_to(mouse_start) > drag_distance:
				dragging = true

If I disable the script on the stage and manually add a item to the game the drag and click functions work fine. I have also tried setting the input as handled but it doesn’t seem to make any difference.

Notes

I’ve previously done some work in Unity, so I’m used to having built in functions that allow you to click on the “top” item in a scene. I’m pretty sure in Unity it works by ray casting from the mouse and stopping on the first collider. Is there something like this in Godot?

Seems like a bit of a gap in the engine that you can’t let the engine itself figure out the difference between a click and a drag etc or to determine itself which object was clicked on. (Unless I’ve missed something, but judging by how many other people I’ve found asking similar things :person_shrugging:)

In Finishing

I’m 100% here to learn so if I’m going down a rabbit hole that doesn’t conform to the way I should be working in Godot please let me know. Thanks in advance.

You can find a recent discussion about this topic here:

Thanks for the info, glad to know I’m not the only one still struggling with this - does this mean that there currently is no solution to this? If it’s something being worked on in the engine is there a workaround for now to get the same sort of functionality, even if it’s a bit of a sledgehammer to open a walnut situation?

In addition to the method mentioned in the other topic, you could also try the following:

  • enable Viewport.physics_object_picking_sort = true in the Viewport
  • make sure (by means of position in the scene tree or by CanvasItem.z_index), that input events are sent first to your items _on_input_event before your stages _on_input_event
  • set an event to handled when processing it in items, so that it doesn’t additionally get sent to your stage.
2 Likes

first add another variable to the scene scripts which acts as a bridge to let the stage know if youre on a collision object other than the stage, secondly change that variable to false when you’re on a collider to disable adding other objects

this works fine with two objects but if you want to take it to n amount then you can add a is_interacting variable to the objects in your scene then make an array of all objects the mouse is colliding with and simply set is_interacting true for the object you want and disable it for the other ones

or you can have your array of objects that the mouse is colliding with, then check which one has a higher z_ordering and simply only interact with the focused object

above method can also go hand to hand with z_ordering so you can only interact with the object which has a higher z_ordering you can only interact with what is above other objects in the scene

hope I wrote it in a comprehensible way and not complete gibberish :smile:

This is what I was trying based on the forum posts that I’d read. I hadn’t set Viewport.physics_object_picking_sort = true before, hadn’t come across that in my research, but I’ve just added it to my code and it doesn’t seem to make any difference.

Maybe I’m not targeting the right viewport? You can see in the code for item I’m trying to set the input event as handled but I don’t know how to check if this is working.

Or maybe the inputs aren’t being sent to objects in the right order? I have looked in the docs but I don’t understand how to prioritise this in the tree (that’s why I tried instantiating as a child to the main scene and the stage scene to see if it made any difference). Have tried setting the stage Z index to -1 while the items have Z-index 0 but the click still passes through and is handled by both the stage and the item.

Here is the updated code for the stage for what it’s worth:

extends StaticBody2D

@export var item : PackedScene
var items : Array

func _ready():
	get_parent().get_viewport().set_physics_object_picking_sort(true)

func _unhandled_input(event):
#func _on_input_event(viewport, event, shape_idx):
#func _input(event):
	if event is InputEventMouseButton and event.is_pressed():
		#print(event)
		var new_item = item.instantiate()
		new_item.global_position = event.position - position
		#new_item.global_position = event.position 
		items.append(new_item)
		add_child(new_item)
		#get_parent().add_child(new_item)

Thanks for the suggestion - will give this a try when I get a chance.

1 Like

well z-index is only for drawing objects meaning its not related to collisions
(apparently it does and its seems more straightforward than what i suggested)

Z-index can influence the collisions. Please have a look at the documentation of Viewport.physics_object_picking_sort.

Try _on_input_event instead of _unhandled_input.

2 Likes

that makes my own code easier as well thnx man :smile:

This worked - with what you were saying before about setting the ordering to true, thanks!!! - I should’ve been more thorough with my testing.

For the benefit of anyone else who was struggling with this here is the code that worked thanks to @Sauermann.

Working example

Both the stage and item objects are StaticBody2D with a Sprite2D and CollisionBody2D - the _on_input_event is signalled from the CollisionBody2D so the clicks match the shape.

(Edit) Oh also - don’t forget to set the objects as pickable under Input in the inspector!!

Code for Stage:

extends StaticBody2D

@export var item : PackedScene

func _ready():
	get_parent().get_viewport().set_physics_object_picking_sort(true)

func _on_input_event(_viewport, event, _shape_idx):
	if event is InputEventMouseButton and event.is_pressed():
		var new_item = item.instantiate()
		new_item.global_position = event.position 
		get_parent().add_child(new_item)

The get_parent().get_viewport().set_physics_object_picking_sort(true) was what was missing from every other post I’ve seen about this. Note that the docs state this is off by default as it can be expensive.

Code for item

extends StaticBody2D

#tracking state
var button_down : bool = false
var dragging : bool = false
#used to offset start position for mouse so item doesn't snap centre to mouse position
var pos_delta : Vector2 = Vector2(0,0)
#used to switch to drag mode if mouse is moved further than limit while button pressed
var drag_distance = 3
var mouse_start : Vector2 = Vector2(0,0)
#used to show clicks
@export var costumes : Array[CompressedTexture2D]
var costume_num = 0

func _physics_process(_delta):
	if dragging:
		drag()

func drag():
	global_position = get_global_mouse_position() + pos_delta

func _on_input_event(viewport, event, _shape_idx):
	viewport.set_input_as_handled() #items with lower z-index will then ignore
	if event is InputEventMouseButton:
		if event.is_pressed():
			button_down = true
			pos_delta = global_position - get_global_mouse_position()
			mouse_start = get_global_mouse_position()
		if event.is_released():
			if button_down and not dragging:
				costume_num = (costume_num + 1) % costumes.size()
				%Sprite2D.texture = costumes[costume_num]
			button_down = false
			dragging = false
	if event is InputEventMouseMotion:
		if button_down and !dragging:
			if get_global_mouse_position().distance_to(mouse_start) > drag_distance:
				dragging = true
3 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.