Skip to content

Instantly share code, notes, and snippets.

@jakebesworth
Last active July 25, 2024 01:24
Show Gist options
  • Save jakebesworth/c6023a0ce584f89083a2703b4b41dbef to your computer and use it in GitHub Desktop.
Save jakebesworth/c6023a0ce584f89083a2703b4b41dbef to your computer and use it in GitHub Desktop.
Overlap clickable Area2D's in Godot. Only click the top most node. Also allows clicking outside of clickable nodes (such as to deselect)

Godot Clickable Overlap Objects and Deselect

Summary of Problem

If you want to have objects in your game be clickable, one of the most obvious methods is using a Area2D > Texture + CollisionShape2D. When your mouse interacts with the CollisionShape2D it has mouse motion, mouse exited, and input events.

Note: Control nodes have restrictions, eat up all mouse events, and don't work well with deselecting by clicking outside of any objects

  1. The biggest issue is when you click on overlapping objects it will signal both events asynchronously, so there is not an easy way to differentiate the top object being clicked.

  2. Also if you want selectable objects, you may wish to know when you click outside of a clickable object to deselect the currently selected object.

image

Solution

  1. To create a clickable object: This requires the group clickableObject, and 3 methods that are attainable through a CollisionShape2D or otherwise
    1. Mouse Motion signal
    2. Mouse exited signal
    3. Function when a click happens (not a signal)
  2. Input being handled in the root script

Every time the mouse enters a clickable object, it will update the current "hovered node" by calling the group, and checking which one is the top most of the group, and sending a click event to it. Note that in godot the top most object in the scene list is the bottom most node returned in get_nodes_in_group(). This method also supports having a "selected node", which you can deselect by clicking outside any non-clickable objects.

Clickable Object (BoxObject.gd)

  • Area2D (group = clickableObject)
    • CollisionShape2D (pickable = true)
      • RectangleShape2D (local to scene = on)
    • TextureRect (mouse = ignore)
extends Area2D

func leftMouseClick():
    get_tree().get_root().selectedElement = self
    print('this object is on the top, and clicked!')
    
func _on_BoxObject_input_event(_viewport, event, _shape_idx):
    if event is InputEventMouseMotion:
        get_tree().get_root().setHoveredNode(self)

func _on_BoxObject_mouse_exited():
    get_tree().get_root().unsetHoveredNode(self)

Root scene node / script (Game.gd)

  • Node2D
    • TextureRect (make sure mouse events are ignore for any control nodes or textures since they both consume mouse clicks)
    • BoxObject1 (bottom most BoxObject)
    • BoxObject2
    • BoxObject3
    • BoxObject4 (top most BoxObject)
    • BlackBlob (another clickableObject group, same methods as BoxObject, top most node in the list)
extends Node2D

var hoveredElement = null
var selectedElement = null

func unsetHoveredNode(node):
    if self.hoveredElement == node:
        self.hoveredElement = null

# Returns boolean if it is the new top hovered object
func setHoveredNode(node1):
    if self.hoveredElement == null or self.hoveredElement == node1:
        self.hoveredElement = node1
        return true
 
    var nodes = get_tree().get_nodes_in_group('clickableObject')
    var hoveredPosition = 0
    var i = 0
    for node in nodes:
        if node == self.hoveredElement:
            hoveredPosition = i
            break
        i += 1

    var node1Position = 0
    i = 0
    for node in nodes:
        if node == node1:
            node1Position = i
            break
        i += 1

    if node1Position >= hoveredPosition:
        self.hoveredElement = node1
        return true
    else:
        return false

func _input(event):
    if event is InputEventMouseButton && event.pressed:
        if event.button_index == BUTTON_LEFT:
            if self.hoveredElement == null:
                # No object is being clicked, so deselect selected node
                self.selectedElement = null
            else:
                # Call left click for only the top object that is being clicked
                self.hoveredElement.leftMouseClick()

image

@jakebesworth
Copy link
Author

Thank you for this solution!

I'm facing another issue right now. I'm trying to use Y-Sorting with this solution but it is not working as expected, as the rendering order changes but not the nodes order in the scene tree. Do you know a workaround for this?

Not a problem. To be honest I'm not too familiar with Y-Sorting so I can't give you any expert solution to that problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment