godot: Can't bind controller trigger axis events (L2, R2) as if buttons (4.0 beta)

Godot version

4.0 beta (<=17)

System information

Arch Linux

Issue description

In godot 3.5, it was possible to bind a trigger (L2 or R2) event to a InputEventJoypadButton. (Through what was button_index 6 and 7) this allowed for watching for a trigger press via InputEvent.is_action_pressed("action") and InputEvent.is_action_released("action")

However, in godot 4 beta (<=17), this functionality has been removed, and you can only bind the triggers as InputEventJoypadMotion (axis 4 and 5). You may still monitor events through InputEvent.is_action_pressed("action") and InputEvent.is_action_released("action")… However, multiple events will be sent, instead of just one like is done with a button or a key without echo events. This seems to be expected behaviour… (other than that i notice InputEvent.is_action_released("action") is true while slowly SQUEEZING the trigger, not when releasing it… the only thing I can think there is that whether an event is being pressed or released is being defined by the axis strength < 0.5 as opposed to the axis strength being changed relative to what it was last it was checked.)

A workaround to this behaviour is to use Input.is_action_just_pressed("action") and Input.is_action_just_pressed("action") instead. However, this is not a complete solution. As this cannot be run inside _input(event: InputEvent) given that it seems the Input singleton is only updated once per process frame I think? Meaning that if you want to handle trigger inputs, and want to allow rebinding of controller inputs… you cannot use the _input(event: InputEvent) function AT ALL, and must watch all of your inputs through the Input Singleton inside of _process()

As I see it, there are two solutions to this problem:

  1. bring back the option to bind (L2 and R2) as InputEventJoypadButton using the same rules they followed in 3.5

  2. change the behaviour of InputEventJoypadMotion.is_action_pressed("action") to check for having just left the defined deadzone and InputEventJoypadMotion.is_action_released("action") to check for having just entered the defined deadzone. Though, I am not sure this would be a desirable change for anyone other than myself, as it would limit the use of InputEvent monitoring through _input(event: InputEvent) in a different way.

Steps to reproduce

I have attached two projects, that try to implement the exact same thing. One is in godot 3.5, while the other uses the 4.0 beta.

You can see the behaviour as it was in 3.5, by using L2 (button 6) which is bound as a InputEventJoypadButton In the 4.0 beta version L2 is not bound, as I can’t find a way to bind it as an InputEventJoypadButton… and as I said above, I suspect that functionality no longer exists. The script tries looking at all button indexes after 15 to see if any of them happen to bind to L2, and none seem to.

In both versions R2 (axis 7 in 3.5, axis 5 in 4.0 beta ) is bound as an InputEventJoypadAxis. This allows looking at an example of the workaround I mention above.

In both you can change from reading input through _input(event: InputEvent) to _process() and back by pressing button 0 (x on a sony controller)

A parity count is kept based on the number of times a button presses or releases for testing purposes, and each project will print the count along with some other information every time the trigger is pressed or released.

Minimal reproduction project

triggers_3_5.zip triggers_4_beta_17.zip

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 4
  • Comments: 17 (4 by maintainers)

Most upvoted comments

The fact that triggers are axes is not going to change (aside from the reasoning above, it would also break compatibility with the previous 4.x versions). We need to think of some solution to resolve this issue. The suggestions here will be considered, but the actual solution might differ.

As I said, since triggers are now axes (which is more correct, because they aren’t boolean input), they work as such and need to be handled differently. Left/right stick actions will yield same/similar results in _unhandled_input(), because axis input is like that. The solution is not using third-party addons but adjusting the code. This works perfectly fine:

func _process(delta):
    if Input.is_action_just_pressed("L2"):
        print("trigger pressed")

How do you propose this should be fixed? Triggers can’t send both Button and Axis events, because it would be inconsistent.

If anyone else is looking for a workaround, I’ve added this as an autoload to re-create the Godot 3.5 behavior in my game. Set up empty input maps (in this case, “use” and “dash”). Then set up input maps that map to the triggers (“use_trigger”, “dash_trigger”). Then this will map the trigger actions to the correct input events:

extends Node

func _process(delta: float) -> void:
	_fix_trigger_action("use_trigger", "use")
	_fix_trigger_action("dash_trigger", "dash")
        # ...plus any other inputs you need to fix
	
func _fix_trigger_action(trigger_action: String, action: String) -> void:
	if Input.is_action_just_pressed(trigger_action):
		_simulate_action(action, true)
	if Input.is_action_just_released(trigger_action):
		_simulate_action(action, false)
		
func _simulate_action(action: String, pressed: bool) -> void:
	var event = InputEventAction.new()
	event.action = action
	event.pressed = pressed
	Input.parse_input_event(event)

Non-trigger inputs, such as the keyboard, can be mapped to the “use” and “dash” events to re-create the universal actions of Godot 3, so that trigger buttons and keyboard buttons can be handled in the same way.

@Nathan-R-Og I have seen this issue with the analog stick as well. I was trying to implement a “double tap to run” feature and my character was sometimes running with a single tap. My workaround was to wait until the deadzone reaches an even lower point than the threshold before accepting another press:

var analog_actions_pressed := {
	"move_left_analog" = false,
	"move_right_analog" = false,
	"move_up_analog" = false,
	"move_down_analog" = false
}

func _unhandled_input(event: InputEvent):
	for analog_action in analog_actions_pressed.keys():
		if event.is_action_pressed(analog_action) && !analog_actions_pressed[analog_action]:
			analog_actions_pressed[analog_action] = true
			# handler code goes here
		elif Input.get_action_raw_strength(analog_action) < 0.1:
			analog_actions_pressed[analog_action] = false

Could something like this be implemented as a back end solution?

@the-eclectic-dyslexic good catch, those early returns that I thought were optimizations were subtle bugs! Fixed, thank you.

I had to check whether or not pressed and released can both fire during the same frame, or if Godot queues them or something, and learned that until recently they could not:

https://github.com/godotengine/godot/pull/77055

@KoBeWi I think I mostly agree with hunter on this one. The way the triggers are treated for the purposes of InputEvent.IsActionPressed("action") don’t make any sense to me, so much so that I thought I just didn’t understand some method behind the madness, and didn’t want to suggest to strongly about changing the behaviour in case someone understood something I didn’t and I broke a bunch of people’s projects who were relying on the seemingly arcane behaviour. In fact they make so little sense I see now that I failed to describe the utter confusion I had when I came across this behavour. That was the purpose of the parity counter in the provided projects. Holding the trigger the right way can easily get you dozens of press actions and zero release actions… or vice versa.

As per above, I am actually much more in favour of InputEvent.IsActionPressed("action") being only true when the deadzone has just been left. Not doing this could be considered okay by me only if some kind of reasoning can be given behind the behaviour as is… additionally that behaviour should be well documented so that people actually know what it does and why. If that doesn’t happen, people are going to do what I did when I posted this issue (trying to use it assuming what I suggested in option 2 of the original post was the behaviour) and then be utterly perplexed as to what is going on when they rebind something to it and find it utterly broken by that behaviour… when the action they had bound by default worked fine despite the strange behaviour. The alternate option of “no you cannot press an axis” also seems fine to me, if no reason for the behaviour can be given, and a different method is used to tackle this issue as you suggested by “the actual solution may differ”.

Also…

@hunterloftis That code snippet is an unreasonably KISS solution, and I am salty I didn’t think of it and worked around it in a hugely verbose way instead. I need to up my godot-fu! I will note though… If I am reading it correctly, I think that will only fix one button per frame. If you press both triggers during the same frame, I don’t think your solution will work as intended. It will only fix your “use_trigger” in that case. Which may be fine for your game, but it is something to consider if you run into a bug with your “dash_trigger” not registering in the future.

Also, thank you for making a stink and bringing attention to this issue!

@Nathan-R-Og Ya, you could write your own input system that you would use ontop of Godot’s action system just for 2 buttons… but I think that is a little bit much to be expected as I wouldn’t really consider that a solution so much as a reliable way to create bugs. Even if your wrapper works perfectly, you need to wrap inputs in every single location that you want to handle inputs. It might be more reasonable if you could place your wrapper directly in the way when the engine needs to call the _input or _unhandled_input methods. Which I am sure could be done with an extension of the engine… but this is kind of ridiculous.

I will readily admit that this issue in particular has been a real thorn in my side. Writing the logic for controller rebinding and only getting to use the Input singleton everywhere I need inputs has not been a good time. For my purposes, the _input and _unhandled_input functions may as well not exist… despite them just being the better option from a code organization standpoint imo.

I see no reason to abandon L2 and R2 button bindings. I use them in my game!

FYI: DUALSHOCK™4 wireless controller