Building Player State Machine, not detecting _unhandled_input

Godot Version

4.6

Question

So, I’m following a tutorial for building a top down player state machine. I have the Idle and Walk states working, but the state machine uses unhandled input to detect button press for Attack. I cannot get the function to fire. Nothing I’ve checked that’s active should be eating my inputs.
the function in the player state machine script:

func _unhandled_input(event: InputEvent) -> void:
	print("unhandled input event: ", event)
	change_state(current_state.handle_input(event))
	pass

func change_state(new_state: State) -> void:
	if new_state == null || new_state == current_state:
		return
	if current_state:
		current_state.exit()
	prev_state = current_state
	current_state = new_state
	current_state.enter()

It should listen for the input event, then poll the current state if it cares about that input. if so it returns the state it should switch to as below, and then kicks off that state.

func handle_input(_event: InputEvent) -> State:
	print("got input: ", _event)
	if _event.is_action_pressed("attack"):
		return attack
	return null

The issue is, I’m not even seeing the first debug print appear, so it’s never even receiving the input. I tried directly telling the state machine change_state(attack) from my player script by doing:

func _process() -> void:
   	if Input.is_action_pressed("attack"):
		state_machine.change_state(attack)

and the state triggered properly.

A. Is there a way to tell what is eating my input so that it isn’t getting down to the state machine as unhandled?

B. From a State Machine design perspective, is there a reason that I should be using unhandled input, instead of something else? (doing if Input.is_action_pressed directly in the process function of the state, for example). The state machine tutorial I’m following is like 2 years old, but most of the newer ones are for side-scrollers and I wanted something closer to what I was making.

GUI could be handling the input, you may be able to check in the Debug panels, “Misc” tab will show you the last thing you clicked on. If your “attack” action is a mouse click then it would make sense for Control nodes to consume it’s input before reaching other nodes. If you can’t sort out what is consuming the input you can use _input instead of _unhandled_input to check all events.

What @gertkeno said. You haven’t told us what is mapped to the “attack” action. A screenshot of the InputMap would be helpful.

Attack is mapped to joypad face button bottom, space bar, and to try to make sure it wasn’t getting handled as “ui_accept”, I also mapped it to the M key which isn’t used for anything by built-ins.

As a test, I added left mouse click, and tested the Misc section and there were some full screen hidden UI menus absorbing my clicks, so I set them to ignore, and tried again and nothing showed up in Misc and it also didn’t fire the unhandled input. The only thing active other than the player that should be listening for inputs are the main.gd listening for pause, and a HUD UI element listening for dpad left and right to swap items:

func _process(delta: float) -> void:
	if Input.is_action_just_pressed("pause"):
		toggle_pause()
		player.is_paused = !player.is_paused

and

func _process(delta: float) -> void:
	#if selected_slot > active_size:
	#	selected_slot = active_size
	if Input.is_action_just_pressed("item_select_right"):
		if active_size != 0:
			if selected_slot != active_size - 1:
				arrow_flash(right_arrow)
				selected_slot += 1
				set_selected_item(selected_slot)
			else:
				return
	if Input.is_action_just_pressed("item_select_left"):
		if active_size != 0:
			if selected_slot != 0:
				arrow_flash(left_arrow)
				selected_slot -= 1
				set_selected_item(selected_slot)
			else:
				return

Also, I tried switching to _input instead of unhandled and it still didn’t fire:

func _input(event: InputEvent) -> void:
	print("input event: ", event)
	change_state(current_state.handle_input(event))
	pass

but if I put a check into the process of the active state, it did fire, passing everything correctly through the same script that can’t detect the _input event :

func process(_delta) -> State:
	if player.input_direction != Vector2.ZERO:
		return move
	if Input.is_action_pressed("attack"):
		return attack
	player.play_anim("idle")
	player.velocity = Vector2.ZERO
	return null

I can bypass the method from the tutorial, but it’s weirding me out that the script can’t seem to hear these built-in functions.

That is really odd.

Can you share your full script? Does it extend a Node type or something else?