godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.07k stars 69 forks source link

Create configurable Joypad axis dead zones #3709

Open madmiraal opened 2 years ago

madmiraal commented 2 years ago

Describe the project you are working on

Any game that uses the joysticks on a joypad.

Describe the problem or limitation you are having in your project

Currently, InputEvents can be mapped to Actions via the Project Settings Input Map tab. Actions have a Deadzone with a default value of 0.5. This DeadZone affects axes i.e. joysticks, in two ways:

  1. The value that must be exceeded for an Action's pressed to return true.
  2. The offset before an Action's strength starts increasing from 0.0 to 1.0, what is commonly understood to be the dead zone.

This dual-purpose is a problem. Although it makes sense to have a trigger point of 0.5 for controlling the point at which an axes sets an Action's pressed to true, it doesn't make sense for the dead zone offset.

There is a need for a configurable dead zone at the InputEvent level. godotengine/godot#43674 highlights that, when a joystick is at rest, there is an expectation that no InputEvents are sent and that the axis value is 0.0. There have been multiple attempts to address this issue. Initially, godotengine/godot#3101 used a filter that dropped all changes less than 1%. More recently, godotengine/godot#55978 dropped changes less then 5%. The problem with filters is that, as godotengine/godot#42876 highlights, there is also an expectation that the sensitivity of the joystick is not reduced by Godot.

Note: To get around the unreasonable value of 0.5 for the dead zone, godotengine/godot#42976 added a get_action_raw_strength() method, but this removes the dead zone instead of setting it to a reasonable level.

Note: Aside from the expectation that a joystick at rest does not send InputEvents and that the value is 0.0, the jitter also aggravates the problem described in godotengine/godot#45628 i.e. that when mapping keys and axes to the same action the Action's pressed is set to false by the axis when just using the keys. This problem is fixed in godotengine/godot#47599.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

A new Project Setting for the size of joystick dead zones. This should have a reasonable default value (e.g. 0.05). Only values greater than this (or less than the negative) are received as InputEventJoypadMotion events.

Note: To avoid confusion, we probably should either rename Actions's Deadzone to Trigger or remove it entirely. Would anyone actually want a value other than 0.5 for being the point at which an axis switches an Action's pressed from true to false?

Note: As discussed in https://github.com/godotengine/godot/pull/42976#issuecomment-714419377 there is an expectation that the dead zone is circular and not square. There may also be an expectation that each joystick has a different dead zone, although I think this is unlikely.

Note: The main driver for previously putting a filter on the axes appears to be as a workaround to the lack of a functioning dead zone. However, there may still be a need to create another Project Setting for joystick sensitivity, which allows setting the minimum step size of the axis InputEvents.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

  1. Create a new Project Setting for the size of joystick dead zones. Set the default value (e.g. 0.05). Ensure that only values greater than this (or less than the negative) are received as InputEventJoypadMotion events.
  2. Remove Actions's Deadzone and ensure that axes switch pressed from true to false at 0.5. Alternatively, rename it to Trigger.
  3. Remove Actions's get_raw_strength() method, as this would no longer be needed.
  4. Optionally ensure that the dead zone for pairs of axes is circular and not square.
  5. Optionally create a new Project Setting for the minimum step size of InputEventJoypadMotion events.

If this enhancement will not be used often, can it be worked around with a few lines of script?

The changes to the Input Map system cannot be done in a few lines of script. However, a user could ignore the Input Map system and monitor InputEvents themselves checking the axis values and ignoring them if they are less than their required dead zone (or even step size).

Furthermore, this feature may not be needed if the only driver for this feature is to address godotengine/godot#45628 i.e. that when mapping keys and axes to the same action the Action's pressed is set to false by the axis when just using the keys. However, neither a dead zone nor a filter will properly fix godotengine/godot#45628, because no matter how big the dead zone or filter, there will always be a nudge big enough to set pressed to false. Note: godotengine/godot#45628 is properly fixed in godotengine/godot#47599.

However, I believe that separating the current dual-purpose of the Action's Deadzone into a trigger and a dead zone, and moving the dead zone to the InputEvent level is an important enhancement.

Is there a reason why this should be core and not an add-on in the asset library?

This affects the core Input and Input Map system.

wareya commented 2 years ago

Question: is there any thought put towards deadzone compensation? e.g. for an axis with a deadzone of 0.15, mapping the input range of 0.15\~1.0 to 0.0\~1.0 as seen by the game. As an option, of course.

Note: As discussed in godotengine/godot#42976 (comment) there is an expectation that the dead zone is circular and not square. There may also be an expectation that each joystick has a different dead zone, although I think this is unlikely.

As I discussed in https://github.com/godotengine/godot-docs/issues/5378, this expectation is not universal, and it shouldn't be Godot's place to play arbiter w/r/t multi-axis deadzone algorithm. Cross-shaped deadzones are sometimes specifically desired. There should be settings for this type of thing.

  1. Remove Actions's Deadzone and ensure that axes switch pressed from true to false at 0.5. Alternatively, rename it to Trigger.

I'm in favor of renaming it. Some games, like shooters, might want to expose trigger threshold configuration to users on a per-action basis, so that they can set up come actions to be hairtrigger sensitive and others to be fully-depressed insensitive. This is technically an accessibility concern.

  1. Remove Actions's get_raw_strength() method, as this would no longer be needed.

Even with a properly-functioning deadzone system, this is still something that you might want to use. For example, if you decide to repurpose your "camera tilt" action as a "move virtual mouse cursor" action, you might want to manually program in a different type of deadzone handling for it. You could set up a second action for this particular example, but there are situations where you might want to keep things to one action per physical control.

Zireael07 commented 2 years ago

+1 to cross shaped deadzones sometimes needed.

madmiraal commented 2 years ago

Question: is there any thought put towards deadzone compensation? e.g. for an axis with a deadzone of 0.15, mapping the input range of 0.15~1.0 to 0.0~1.0 as seen by the game. As an option, of course.

Yes. With a dead zone of 0.15, I think the input range of 0.15-1.0 should linearly increase from 0.0-1.0. There should be no snapping or jumping from 0 to 0.15. Looking at the links in https://github.com/godotengine/godot-docs/issues/5378, the main argument against cross-shaped dead zones is the naïve snapping solution.

golddotasksquestions commented 2 years ago

I am very concerned and confused by this proposal.

Maybe I have not fully understood this proposal, but it reads to me as if you would propose to completely remove individual deadzones from actions and instead just a one general trigger value for all actions. For joysticks you seem to propose a circular trigger threshold.

If that would be the case it would have a number of problems and deficits compared to the existing deadzone:

Deadzones are set not only by the dev in the project settings but are ideally exposed to the players: Every player has unique controllers with unique wear and tear pattern and therefore needs to be able to set this values for each strength-button and joysick individually. Since deadzones are currently part of the InputMap it's easy to use and save custom profiles for players and their individual hardware (controllers). If you would remove individual per-action Deazone from the Input map, Godot devs would need to write custom code, making this kind of feature harder to implement than it is right now.

Circular deadzone alone would be highly problematic. Axis deadzones need to be the default as it represents player behaviour expectations (being able to move or aim straight). Ideally as Godot devs we should have both for analog joysticks: axis Deadzone plus circular Deadzone and the ability to set/adjust both independently from each other and per action.

madmiraal commented 2 years ago

@golddotasksquestions The dead zones need to be configurable. The question is: where? The options are:

  1. A single project setting.
  2. A default project setting, with the option to change the dead zone on each input axis.
  3. On each action.

I think option 2 is the preferred option. Option 1, as you explain, would not allow for changes to be made in game or independently for each axis. Option 3, is the current approach (although it should still be separated from the trigger point), and would allow different dead zones to be set on each half axis, but I don't think this is necessary.

Circular deadzone alone would be highly problematic.

I agree. As you elaborate in godotengine/godot-docs#5378 and @KoBeWi highlights in godotengine/godot#55264, there is an expectation that joystick dead zones are cross-shaped.

golddotasksquestions commented 2 years ago

I would think deadzones are a requirement for any strength based input, not just analog stick axis. Other than that I think I understood you now and I fully agree with you @madmiraal

Tony-Goat commented 2 years ago

I was actually thinking about submitting a proposal on this, but I found this one and I'll weigh in.

Circular deadzone alone would be highly problematic

there is an expectation that joystick dead zones are cross-shaped.

Well, depends on the game or system you're developing. A platformer or top down RPG might expect a cross shaped deadzone, while a twin-stick shooter may require a circular deadzone for accurate aiming controls.

I came across this issue developing a prototype for a platformer. Normal movement necessitates the cross shaped deadzone, however, the player can stop to throw a grapple, which requires a circular deadzone, so some games could require both interchangeably.

It would be easier if the cross-shaped deadzone and circular deadzone were applied at different levels. The circular deadzone would be applied to the joystick before any input mapping, and the cross-shaped deadzone would be applied at the input mapping level.

For example, the deadzone for my current Input.get_axis("player_left", "player_right") looks like this: lr-no-radial

But I could turn on the radial deadzone, which would be applied before the player_left or player_right mapping deadzone, then get a deadzone that looks like this: lr-with-radial

Then I could dynamically change the deadzone for player_left and player_right to 0 during gameplay to get a deadzone that looks like this when the player wants to throw the grapple: radial

Looking at actual implementation, it would definitely require quite a bit of work as the Input pump only looks at one axis at a time to make a decision, and it'd have to change to look at two so that it can properly calculate the distance from the middle of the joystick to figure out if it's in the radial deadzone.

Tony-Goat commented 2 years ago

Also, if you want to do a radial deadzone, you could insert a radial deadzone check in any _input function that requires a radial deadzone.

First, you'd create input mappings to your joystick called joy_up, jown_down, joy_left, and joy_right with no deadzone in the mapping, then you start your _input or _unhandled_input function with logic like this:

const RADIAL_DEADZONE = 0.05

func _input(event):
    var vaxis = Input.get_axis("joy_up", "joy_down")
    var haxis = Input.get_axis("joy_left", "joy_right")

    var magnitude = sqrt((vaxis * vaxis) + (haxis * haxis))
    if magnitude > RADIAL_DEADZONE:
        # Joystick input logic here...

But I can understand how copying and pasting this into every node that handles input can clutter up your codebase, especially if you have to change RADIAL_DEADZONE on every node. And this also won't override any other mapped input deadzones, so you'll have to change that to 0 as well (or your desired setting) for any other inputs that use the same joystick.

groud commented 2 years ago

We discussed today this topic during the proposal meeting. We bikeshed a little bit on it but there seem to be no clear consensus. It seems like a quick fix, at least for the deadzone shape, would be to add an optional argument to the Input.get_vector(...) that would let you define the shape of the deadzone (it would be an enum)

Also, it seems like the current system kind of leads to some sort of confusion. On the one hand, the deadzone is used as a way to filter out garbage events sent by your OS (for joysticks), but on the other hand it is used as a trigger value for boolean (pressed/not pressed) actions.

Having a second look at the original proposal, I personally (this was not discussed in the meeting), think that it is mostly good. I also wonder if, at some point, we should also split "boolean actions" from "analog actions", so that we would only need to assign a trigger value to boolean actions. We would also rework the API so that you could only retrieve the respective type value depending on the action type (pressed status for boolean actions and strength for analog ones). But that's kind of a bigger change, so I am not sure.

moonraymurray commented 1 year ago

So is there any work around for this problem? currently having a PS4 Controller that isn't adhering to deadzone, bought a new controller and tested it too, it's not the controller, it is definitely something to do with the dead zone.