Quick answer: A Control handled the click visually but never marked the event as handled, so Godot kept propagating it into _input and _unhandled_input where your world scripts grabbed it too. Set mouse_filter to Stop, call accept_event() inside _gui_input (or get_viewport().set_input_as_handled() elsewhere), and move gameplay listeners from _input to _unhandled_input.

Here is how to fix Godot input events that refuse to stay consumed. You click a HUD button to open the inventory and the player also shoots. You drag a slider and the camera pans. You confirm a dialog and the dialog opens again because the same click fires the “interact” action one frame later. The Control did its job, but the event kept travelling through the input pipeline and your gameplay code happily processed it again. The cause is almost always one of three things: the wrong mouse_filter, the wrong handler override, or a missing set_input_as_handled call.

The Symptom

You have a UI panel sitting on top of your world. A click on the panel triggers a UI action correctly — the button highlights, the menu opens, the slider moves. But the same click also triggers a world action: the player shoots, the camera rotates, an item gets picked up, or a movement order is issued. Keyboard shortcuts behave similarly: pressing E while a text field has focus inserts the letter and activates the “interact” action.

You confirm with a print statement that _gui_input ran on the Control, then _input ran on the player, then _unhandled_input ran on the camera. All three saw the same InputEventMouseButton. Nothing stopped the chain.

What Causes This

mouse_filter is set to Pass. Every Control node has a mouse_filter property with three values: Stop, Pass, and Ignore. Stop consumes the event after _gui_input runs. Pass calls _gui_input but lets the event continue bubbling to siblings and to the global input queue. Ignore skips hit-testing entirely. The default for many container types is Pass, which is exactly why your HUD looks transparent to clicks — it is.

Gameplay scripts use _input instead of _unhandled_input. The two callbacks look almost identical, but their position in the pipeline is different. _input runs before the GUI subsystem decides whether anything was handled. _unhandled_input runs after. If your player controller listens in _input, no UI in the world can ever block it because it sees the event before the UI does.

accept_event was never called. Even with mouse_filter = Stop, certain custom _gui_input implementations forget to call accept_event(). The Stop filter prevents pointer events from leaking to siblings, but action and keyboard events do not flow through the same path — they use the focus system, and only accept_event (or set_input_as_handled) marks them complete.

CanvasLayer ordering is wrong. A CanvasLayer with a lower layer value renders behind one with a higher value, but input is processed top-down by layer. If your gameplay HUD lives on layer 0 and a popup lives on layer 10, the popup gets first crack at the event. Reverse that and your popup never sees the click. Combine that with mouse_filter = Pass and the result is a click that visually hits the popup but logically reaches the HUD underneath.

Subviewports add an extra hop. If your UI is rendered into a SubViewport, you must explicitly forward unhandled input from the parent into the subviewport, or the SubViewport’s controls never receive anything. People then add a fallback handler in _input “just to make it work,” which guarantees double-firing once the SubViewport routing is fixed.

The Fix

Step 1: Audit mouse_filter on every UI parent. Open your scene and select each Control that should block clicks — panels, dialog backgrounds, full-screen menus, draggable windows. Set mouse_filter to Stop. For purely decorative overlays that should not block input (a vignette, a tutorial arrow), use Ignore. Reserve Pass for cases where you actually want the parent to inspect the event but still let children handle it.

Step 2: Mark events handled inside the right callback. If you handle a click in a Control’s _gui_input, call accept_event(). If you handle a global key in some manager node’s _input, call get_viewport().set_input_as_handled(). They do the same thing — both flip the “handled” flag on the current event so _unhandled_input listeners are skipped.

# A custom HUD button that consumes its clicks correctly
extends Control

signal activated

func _ready() -> void:
    mouse_filter = Control.MOUSE_FILTER_STOP
    focus_mode = Control.FOCUS_ALL

func _gui_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.pressed:
        if event.button_index == MOUSE_BUTTON_LEFT:
            activated.emit()
            # Required: stop further propagation
            accept_event()

func _input(event: InputEvent) -> void:
    # Keyboard shortcut: only fire when this control has focus
    if has_focus() and event.is_action_pressed("ui_accept"):
        activated.emit()
        get_viewport().set_input_as_handled()

Step 3: Move gameplay listeners to _unhandled_input. Open your player controller, weapon script, camera, and interaction system. Anywhere you wrote func _input(event), change it to func _unhandled_input(event). The signature is identical and the events you receive are exactly the ones the UI did not consume. This single change fixes most “UI bleed” bugs without touching anything else.

# Player controller: listens only to events the UI did not consume
extends CharacterBody2D

@onready var weapon: Node = $Weapon

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("shoot"):
        weapon.fire()
        get_viewport().set_input_as_handled()

    elif event.is_action_pressed("interact"):
        var target := find_interaction_target()
        if target:
            target.interact()
            get_viewport().set_input_as_handled()

Calling set_input_as_handled from gameplay code is still useful: it prevents other gameplay nodes that also listen in _unhandled_input from acting on the same event. Without it, a single shoot action could fire the weapon and trigger an item pickup at the same time.

CanvasLayer and Input Priority

Input is dispatched in a specific order: _input on every node (deeper nodes first), then _gui_input on the topmost Control under the pointer, then _unhandled_input on every node (deeper nodes first). Within _input and _unhandled_input, you can adjust process_priority on the node so it runs earlier or later than its siblings. Use this when you need a global menu hotkey to win against gameplay even when the menu is closed: give the menu a lower process_priority so it sees the event first, then call set_input_as_handled when it opens.

For CanvasLayers, set the layer property so popups sit above the main HUD, and ensure each layer’s root Control has the right mouse_filter. A common mistake is putting a transparent Control as the root of a popup layer with mouse_filter = Pass — the click visually hits the popup but logically passes through to the world.

Debugging the Pipeline

When events are still leaking, instrument every callback with a print that includes the node name and the event type. Run the game, click once, and read the order of prints. The first node to claim the event should be the last one printed before silence. If the prints continue past that point, you found the leak: that node either forgot to call set_input_as_handled or it is listening in _input instead of _unhandled_input.

“Every input bug in Godot is one of three things: the wrong filter, the wrong callback, or the missing handled flag. Once you internalise that, you stop guessing and start fixing.”

Related Issues

If your action triggers multiple times per press, see Godot Input Action Just Pressed Multiple Times for echo handling. If actions stop working after switching scenes, check Input Actions Not Working After Scene Change for InputMap reload patterns.

If gameplay code lives in _unhandled_input and UI calls accept_event, your input bugs disappear overnight.