Node2D double click detection in Godot

Two mice using a computer mouse

Clicks for Node2D based nodes can easily be detected by using CollisionObject2D‘s input_event signal.

To capture the mouse clicks we can add an Area2D node and a CollisionShape2D or CollisionPolygon2D as its child node.

Since we are using CollisionObject2D‘s input_event signal directly the Area2D‘s Monitoring and Monitorable won’t be used and we can disable those.

Area2D‘s ‘Collision > Layer needs to be set to some value or the clicks won’t be detected.

The Area2D node also needs to have Input > Pickable set to True.

Give the CollisionShape2D a Shape, eg. RectangleShape2D, CircleShape2D, CapsuleShape2D, etc. and set its Size to the dimensions you want. This defines the area’s size and shape that can be clicked with the mouse.

Finally set the CollisionShape2D‘s Transform > Position to the location where you want it.

Now we can add a script to the Area2D node and in Area2D‘s Node > Signals connect the input_event to the script.

See example below.

public void OnContainerBeingClicked(Node viewport, InputEvent @event, int shapeIdx) {  
  
    // Return if not mouse left button double click.  
    if (@event is not InputEventMouseButton mouseButtonEvent) return;  
    if (!mouseButtonEvent.Pressed) return;  
    if (mouseButtonEvent.ButtonIndex != MouseButton.Left) return;  
    if (!mouseButtonEvent.DoubleClick) return;  
  
    ToggleContainer();  
}

The code is pretty self-explanatory, but let’s break it down.

In the method signature we need to return void, and accept Node, InputEvent, and int as parameters - even if we don’t use them all. It’s simply required to connect the signal.

Since event is a reserved keyword we are required to place @ in front of it if we want to use that particular name in our parameters, ie. @event instead of event. But we might as well use any other name such as inputEvent if it strikes your fancy - it really doesn’t matter. The parameter names can be what ever you want as long as the types match Node, InputEvent and int. The signal will still work.

We cannot directly handle InputEventMouseButton, so we need to use InputEvent instead and cast it to InputEventMouseButton:

if (@event is not InputEventMouseButton mouseButtonEvent) return;  

Here we check whether @event is or isn’t InputEventMouseButton. If it’s not we simply return and do nothing further with that particular input event - someone else can handle that one, what ever it is.

If the @event is InputEventMouseButton we cast it as InputEventMouseButton and expose the cast as a local variable called mouseButtonEvent and continue executing our code. We can now access the InputEventMouseButton mouse button event via mouseButtonEvent and do further things.

if (!mouseButtonEvent.Pressed) return;

Here we immediately put it to use and check if the mouse button is not being pressed (ie. it’s likely being released instead), we will simply return and call it quits there - it’s not our concern.

if (mouseButtonEvent.ButtonIndex != MouseButton.Left) return;

We still don’t know what kind of mouse button press this was, so we can check by using the ButtonIndex whether it was something else than a left mouse button press - in which case we simply return and call it quits again. It’s not our concern, since in this example we want to handle left mouse buttons clicks only.

if (!mouseButtonEvent.DoubleClick) return;

Finally we have a very handy way of checking whether a double click happened without having to deal with timers and special logic each time - we can simply check the DoubleClick property. Godot will expose to us automatically the information whether it was a double click or not. If it’s not a double click, again, we return and call it quits there.

Now we’ve determined:

  1. It is a mouse button event.
  2. It is a mouse button being pressed down event.
  3. It is a left mouse button event.
  4. It is a double click

So we know the user did a left mouse button double click, so we can now do what ever we want with that information.

ToggleContainer();

In the above example what we do is we call our own custom ToggleContainer() method which opens - or closes - an inventory container window for us.

But you can call your own methods or do what ever you want once you’ve ruled out all the cases you don’t want to react to like we did above one step at a time.

You can also combine the if-statements if you like. We have kept them separate in the example for additional clarity and readability.

But you might also do this:

if (@event is not InputEventMouseButton mouseButtonEvent  
    || !mouseButtonEvent.Pressed  
    || mouseButtonEvent.ButtonIndex != MouseButton.Left  
    || !mouseButtonEvent.DoubleClick) {  
    return;  
}

The IL code produced by either of these examples is slightly different, but not meaningfully so.

It’s also worth noting that when using returns, like in the first example, the stack traces can be more meaningful when tracking down problems.

But generally the second example would execute faster. But this would be in the realm of nano optimizations and might be made a moot point anyway by future updates to the runtime.

It’s also worth noting that in Godot 4 you can implicitly cast the Node by accepting Viewport instead of Node in the signal’s method signature as a parameter - and it will still work because Viewport is a child of Node.

So if you want to handle the input, for example, so it doesn’t propagate any further - and you don’t want to cast the Node in your code explicitly to Viewport which you need to access the SetInputAsHandled() method, you can accept a Viewport directly instead of Node which is declared in the input_event signal’s signature.

Eg.

public void OnContainerBeingClicked(Viewport viewport, InputEvent @event, int shapeIdx) {  
  
    // Return if not mouse left button double click.  
    if (@event is not InputEventMouseButton mouseButtonEvent) return;  
    if (!mouseButtonEvent.Pressed) return;  
    if (mouseButtonEvent.ButtonIndex != MouseButton.Left) return;  
    if (!mouseButtonEvent.DoubleClick) return;  
      
    viewport.SetInputAsHandled();  
    ToggleContainer();  
}

This will also work. Otherwise we would have to:

public void OnContainerBeingClicked(Node viewport, InputEvent @event, int shapeIdx) {  
  
    // Return if not mouse left button double click.  
    if (@event is not InputEventMouseButton mouseButtonEvent) return;  
    if (!mouseButtonEvent.Pressed) return;  
    if (mouseButtonEvent.ButtonIndex != MouseButton.Left) return;  
    if (!mouseButtonEvent.DoubleClick) return;  
      
    ((Viewport)viewport).SetInputAsHandled();  
    ToggleContainer();  
}

That’s mostly it for handling mouse double clicks in Node2D nodes.