How to have CollisionObject2D._input_event() only trigger for the topmost collision object?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By scmccarthy

I am using an Area2D with a CollisionShape2D to get mouse clicks, by connecting my handler to the _input_event signal defined by CollisionObject2D. (Yes, I have “Pickable” checked so that the Area2D can get input events.)

(Going by this reference page, this is the lowest priority thing to do with an input event and will only happen if nothing at a higher priority handled the event.)

This works perfectly well in the simplest case, but to my confusion, when I have two instances of this node overlapping each other, both of them get the mouse events. Calling “get_tree().set_input_as_handled()” in the handler does not prevent the second instance from also getting the mouse event.

For reference, here is my handler function:

func _on_Hitbox_input_event(viewport, event, shape_idx):
	if event is InputEventMouseButton:
		if event.button_index == BUTTON_LEFT and event.pressed:
			held_by_cursor = true
			get_tree().set_input_as_handled()

(Hitbox is the name of the Area2D node. This handler is connected to the input_event signal in the GUI.)

What am I missing? Are all applicable CollisionObject2Ds supposed to receive the event even after it’s marked as handled? Also, I thought this was done by casting a ray - shouldn’t the ray only hit one thing in the first place? I would really like to understand how this works. Finally, as a practical matter, how can I limit the click to being handled by just the topmost node that it hits (using the engine rather than rolling my own)?

Thanks for your help.

:bust_in_silhouette: Reply From: scmccarthy

I looked into the engine code that deals with input events. The short answer is that what I want is impossible. Yes, once the event is marked as handled, it won’t be sent to anyone else. But the physics system counts as a single entity for this - basically if the event doesn’t get handled prior to that stage, it gets dumped into a separate queue that has its own rules.

The physics system notifies every physics object (well, up to the first 64) that’s under the mouse. It has no way of knowing if a previous physics object tried to handle the event or didn’t. Thus if you want only the topmost physics object to react, you’ll have to code that yourself.

So to answer my questions:

What am I missing?
Nothing.

Are all applicable CollisionObject2Ds supposed to receive the event even after it’s marked as handled?
Not quite right. If it’s marked as handled before this point, none will receive it. But marking it as handled doesn’t work during this stage.

Also, I thought this was done by casting a ray - shouldn’t the ray only hit one thing in the first place?
No, there are legitimate reasons for wanting a ray to hit multiple things. In this case the ray is set to hit up to 64 objects.

How can I limit the click to being handled by just the topmost node that it hits (using the engine rather than rolling my own)?
You can’t.

Edit: Important addendum. The raycasting doesn’t find objects in their actual z order anyway, so this appears to be entirely the wrong approach.

I do wonder if this is a bug since the page explaining how the engine does this says:

If no one wanted the event so far, and a Camera is assigned to the Viewport, a ray to the physics world (in the ray direction from the click) will be cast. If this ray hits an object, it will call the CollisionObject._input_event() function in the relevant physics object (bodies receive this callback by default, but areas do not. This can be configured through Area properties).

:bust_in_silhouette: Reply From: adabru

Hi @scmccarthy, I had the same problem. Thank you for sharing your research! As you already mentioned, I’m using a workaround. For those interested, instead of using _input_event I did following:

  • add new CollisionShape2D with name “Collider” to the Area2D
  • make :heavy_check_mark: on "Collider"s property Disabled
  • set "Collider"s shape to CircleShape2D with radius 1 to simulate mouse
  • add following code to the Area2D-script:
func _unhandled_input(event):
  if event is InputEventMouseButton and event.pressed:
    # check hitbox collision
    $Collider.position = self.global_transform.xform_inv(event.position)
    if $Collider.shape.collide($Collider.global_transform,
      $Hitbox.get_shape(), $Hitbox.global_transform):
      ...
      get_tree().set_input_as_handled()

many thanks, that’s exactly what I was looking for.
It’s a pity to have to do that, but it works perfectly and you can go through your objects the way you want to, using for node in get_tree().get_nodes_in_group("my_group"):

I used the above to set the collider’s position:

collider.position = get_global_mouse_position()

jeyzu | 2019-03-21 20:32

:bust_in_silhouette: Reply From: siska

I’m having the same problem as well.

I have 2 different kinds of clickable objects that can sometimes overlap. I want the mouse click to only register on the topmost object ; this will always be the node that is lower in the scene tree (z-index sorting).

I solved the problem by making that topmost clickable object a TextureButton. If that object is not a rectangular shape, I use a TextureButton click mask to only allow for clicks on the opaque regions of my sprite.

:bust_in_silhouette: Reply From: smbridges

An option that worked for me was to use the mouse_entered and mouse_exited signals to keep track of which Area2D nodes were under the mouse cursor. Then I used _unhandled_input to handle the actual button click. This allows set_input_as_handled() to stop the signal from propagating to the other nodes.

var is_mouse_over = false
var is_held = false

func _on_Area2D_mouse_entered():
  is_mouse_over = true

func _on_Area2D_mouse_exited():
  is_mouse_over = false

func _unhandled_input(event):
  if is_mouse_over:
    if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
      is_held = event.pressed
      get_tree().set_input_as_handled()

Be aware, the input is processed in the reverse depth order of the scene tree, not in z index order.