InputEvent Action Name Matching With InputMap

Godot Version

Godot Engine v4.3.stable.official

Question

Hi all, I’m trying to figure out how I might be able to implement my desired input system. Currently I have a mapper class that is trying to access the name of the event (as it’s defined in the InputMap) in order to identify the type of action it is (ability or active item).

I currently have these inputs defined as “Player_Ability_X” and “Player_Item_Y” and want to be able to identify on or the other as shown below:

        # - InputMapHelper.gd
	playerActions = InputMap.get_actions().filter(func(action):
		return (action.contains("Player_Ability") or
                             action.contains("Player_Item"))
	)

        # - InputMapper.gd
        func _input(event: InputEvent) -> void:
		if eventIsPlayerAction(event):
			var actionName = event.name.split("_")
			match actionName[1]:
				"Ability":
					player_ability_event.emit(actionName[2])
				"Item":
					player_item_event.emit(actionName[2])

I tried using InputEventAction, but I didn’t realize that had to be manually emitted, is there any other way I might be able to achieve this? The goal is to be able to emit the same signal with an index parameter so that the handler can easily seek the intended logic without the need to make exhaustive updates to multiple files when a new action/input is added or supported.

Thanks in advance, sorry if my explanation is unclear.

What needs to be manually emitted? It seems like a good script, if you could explain further what is going wrong with it that would be very helpful.

Thanks for the quick reply!

In the docs for the InputEventAction, it seems like this event isn’t emitted when the user presses the physical key, so I assume it’s meant for the developer to hook this into their project through the Input.parse_input_event function to artificially trigger an input event.

Really I’m currently having issues figuring out how I can get the actual match to work with the user’s physical input. I can’t figure out how to receive the name as I was intending since I was hoping to intercept the physical key press from the InputMap, but InputEventAction doesn’t seem to support that. Do you know any workarounds that might be able to help me achieve this function? I’ve seen in the docs that I should be using InputEvent.is_action_pressed() and the likes, but I was really hoping I would be able to do it in the manner I outlined above.

Again, thanks for the reply. I hope my reply offers more clarity on my intentions.

Ah thanks! InputEventActions are emitted to the _input function, the tricky part is the function usually recieves the base type of all events, InputEvent. You have to down-cast/type check the event against the exteneded type. Try this:

func _input(event: InputEvent) -> void:
    if event is InputEventAction: # type check / down cast
        var action_split := event.action.split("_") # action, not name it seems

        if action_split.size() == 3:
            if action_split[1] == "Ability":
                player_ability_event.emit(action_split[2])
            elif action_split[1] == "Item":
                player_item_event.emit(action_split[2])

Hey, thanks again for the quick reply.

It slipped my mind but I should’ve mentioned it before, I did try to cast it as an InputEventAction like this:

var eventAction = event as InputEventAction
var actionName = eventAction.action.split("_")

But it unfortunately crashes when trying to access the eventAction.action because it comes back as null. Probably because the physical event isn’t castable/compatible to the InputEventAction type since it seems to have some unique logic tied to it.

However, when I was typing my last reply I got the idea that I could use code similar to below:

func abilityEventIdentifier(event: InputEvent):
    for ability in InputMapConvenience.playerAbilities:
        var abilityString = ability.split("_")
        if event.is_action(ability)
            player_ability_event.emit(ability[2])

But I’m worried about the performance implications this would have, especially when the player would press the buttons in rapid succession. I would vastly prefer the initial implementation as it would be constant time complexity and this one is linear. Realistically, it would be a negligible difference, but I also just think it’s a bit on the messier side as well.

Any thoughts on how I might be able to implement my initial approach or any optimizations you could think of through other approaches? Thanks for your thoughts!

Edit: I forgot maps existed, the second implementation looks a lot better if we make the InputMapConvenience.playerAbilities as a map rather than an array.

Edit #2: Nevermind, I forgot that I can’t access the name of the mapped event, a for loop would still be required to evaluate the event.is_action(x) with all known instances of playerAbilities or items.

I’ve revisited the contents of my latest reply, it is still possible, but it’s a convoluted and hacky solution. I’ve created a dictionary with the event key as the key and InputMap name as the value.

Implementation below:

# - InputMapHelper.gd
func _populatePlayerActions():
	for action in InputMap.get_actions():
		if action.begins_with("Player_Ability_") or action.begins_with("Player_Item_"):
			_playerActions.get_or_add(InputMap.action_get_events(action)[0].as_text(), action)

func isPlayerItem(event: InputEvent) -> bool:
	return _playerActions.get(event.as_text() + " (Physical)").begins_with("Player_Item_")

func getEventIndex(event: InputEvent) -> int:
	return _playerActions.get(event.as_text() + " (Physical)").split("_")[2].to_int()

# - InputMapper.gd
func _input(event: InputEvent) -> void:
	if InputMapConvenience.isPlayerItem(event):
		player_item_event.emit(InputMapConvenience.getEventIndex(event))

I still don’t like this solution much. We need to add " (Physical)" to the event.as_text() because for some reason when you receive the inputEvent from the InputMap, it specifies that this event is in response to a physical key press. Magical strings are a big no no for me, so this solution is terrible to me. Furthermore, the specification for (Physical) is concerning because I don’t know what this means for software keyboards or other methods of inputs.

I still hope for a solution that is closer to my initial desires if anyone could take a crack at it, it would be appreciated. Thanks!

Edit: For the dictionary keys, I changed the key to be stored as InputMap.action_get_events(action)[0].as_text().split(" ")[0] so that we could remove the (Physical) specification. I still think it looks bad though.

Not all inputs passed will be InputEventActions, so a mouse movement event would trigger that crash for example. That’s why the if statement is important, only splitting the action when it really is an action. as will return null if the variable is not down-castable.

I went ahead and tried this code snippet to verify:

	if event.is_pressed() and event.is_action_type() and event.is_action("Player_Ability_1"):
		if event as InputEventAction:
			print("It is an InputEventAction")

Unfortunately this is still returning as null. In my if statement, I specified that it needs to be one of my expected ability indices, but the event is still not castable to be an InputEventAction. Is this how you meant?

I see what you mean, and I see how it can’t work the way I posted. One input can trigger multiple actions, Godot tracks the single input instead of duplicating it for multiple actions, thus event.action couldn’t simply describe the action unless it was already tied to a specific one like InputEventAction must be created.

This snippet from another thread seems a good place to start, you want to also filter actions that don’t start with “Player_Action_” or “Player_Item_”

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_type() and event.is_pressed():
		var actions = InputMap.get_actions().filter(func(action:StringName):
			var is_player: bool = action.begins_with("Player_Action_") or action.begins_with("Player_Item_")
			return is_player and event.is_action(action, true))

		for action in actions:
			var actionName = action.split("_")
			match actionName[1]:
				"Ability":
					player_ability_event.emit(actionName[2])
				"Item":
					player_item_event.emit(actionName[2])

I see, so in the end it seems like the least roundabout way is to still iterate through the array of InputMap events. In that case I think I’ll just opt for this implementation then. Thank you for your input @gertkeno !

In conclusion, the implementation that is closest to my desired solution is as follows:

func _populatePlayerActions():
	for action in InputMap.get_actions():
		if action.begins_with("Player_Ability_") or action.begins_with("Player_Item_"):
			_playerActions.get_or_add(InputMap.action_get_events(action)[0].as_text().split(" ")[0],
										action)
	print(_playerActions.keys())

func isPlayerItem(event: InputEvent) -> bool:
	return _playerActions.get(event.as_text()).begins_with("Player_Item_")

func isPlayerAbility(event: InputEvent) -> bool:
	return _playerActions.get(event.as_text()).begins_with("Player_Ability_")

func getEventIndex(event: InputEvent) -> int:
	return _playerActions.get(event.as_text()).split("_")[2].to_int()

# - InputMapper.gd
func _input(event: InputEvent) -> void:
	if not event.is_pressed():
		return
	if InputMapConvenience.isPlayerAbility(event):
		player_ability_event.emit(InputMapConvenience.getEventIndex(event))
	elif InputMapConvenience.isPlayerItem(event):
		player_item_event.emit(InputMapConvenience.getEventIndex(event))

Again, thanks @gertkeno for helping me with ideas! Hopefully this helps others that have the same issues I did.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.