legacyclonk / LegacyClonk

The LegacyClonk engine and the c4group command line tool.
https://clonkspot.org/lc-en
Other
83 stars 18 forks source link

Improved support for controller / gamepad #115

Open fritzw opened 9 months ago

fritzw commented 9 months ago

The current gamepad user experience is somewhat suboptimal, at least for players who played any recent platformer with a gamepad.

Firstly, Clonk does not support XInput, so no support for "modern" controllers like the various XBox types. This can be fixed using Xidi. This works, but you see no actual button names in the UI, only button numbers (e.g. on the action indicators at the bottom right of the screen). Actual XInput support would be nice, and it "should not be that hard" (haha). Just call XInputGetState instead of joyGetPosEx (but of course the devil is in the details).

But second, and more importantly: Nearly all modern games follow the same conventions for gamepad control for good reasons:

  1. The A button on an XBox style controller is used for 2 actions: Confirm (in menus) and Jump (in game).
  2. The Up direction of the analog stick and on the D-pad are never used to jump, because this causes undesired jumps in hectic situations, but it is used for things like climbing or swimming upwards.
  3. The B button is used for: Cancel or Back (in menus) and some (maybe configurable) action in game.

But in Clonk, this setup is currently not possible. You could configure the A button as the "Up" key in Clonk, allowing you to jump with A. But "Confirm" in menus is always the same as "Throw", so you can't have "Confirm" and "Jump" both on A. Also, you would have to press A to climb up or navigate upwards in menus. You can use Xidi to map both A and Up to the clonk "Up" key to fix that issue and still have Jump on A, but then you still have the problem that you can get accidental jumps when you walk around, by pressing the analog stick slightly in the wrong direction.

The 3rd point is not so much of a problem, because the "Cancel" button in menus is the same as "Dig". Mapping that to the B button works fine, although it would still be nice to have the option to configure "Dig" independently of "Cancel".

The ideal solution in my opinion would be to have "Up" and "Jump" (and possibly even "Menu Cancel") as separately configurable inputs (which could be configured to the same input of course). A quick look at the code tells me that this is probably not so easy, because some code depends on the notion that "Up" is the same as "Jump", for example ObjectComUp directly calls PlayerObjectCommand(cObj->Owner, C4CMD_Jump). I expect there to be more such places, but I didn't look further than that, so I might be mistaken. Some problematic cases might even be in some c4script code somewhere...

If someone changed the code anyways, it might also be desirable to configure "Menu Cancel" and "Dig" as separate keys.

maxmitti commented 9 months ago

Last time I checked, XInput actually worked, when I pressed some button on the gamepad before starting Clonk (or restarting it afterwards), or something similar. The same was also true for other old games like Need for Speed Underground 2.

It might still have been in Windows 7, but I would hope that Microsoft didn’t drop that compatibility in newer versions.

All the button and axis names are definitely not helpful at all. I wonder how much better it would be when using SDLs Gamepad support.

Splitting menu actions from the normal controls is an interesting idea. There is already some special handling for the mouse, which could be helpful. Fortunately, in this case, the menu handling functionality for scripts is quite limited. So there should not be scripting issues, I think.

fritzw commented 9 months ago

Well, at first I also thought that it kinda works, because I could control the menu in Clonk with my XBox 360 controller, even without Xidi. But it turned out that it was only because Steam was running in the background and was translating controller inputs to keyboard inputs like the arrow keys. It's called "Desktop Layout" in the Steam controller settings, and after I set that to the "Disabled" layout, the controller did nothing in Clonk without Xidi. Even playing was possible (to a degree) when selecting the "Keyboard 4" controls for my player, but more customizations would be necessary in Steam to make it work really.

Oh, in case anyone is interested, here's my current Xidi.ini which maps the A button to "Up" on the left stick:

[Mapper]
Type                                = JumpOnA

[CustomMapper:JumpOnA]
Template            = DigitalGamepad
ButtonA             = Axis(Y, -)
maxmitti commented 9 months ago

No, it was definitely not Steam, and I was using the Gamepad 1 option in Clonk.

maxmitti commented 9 months ago

Which Windows version are you using?

fritzw commented 9 months ago

This is on Windows 11.

fritzw commented 9 months ago

I've added a checklist to the first comment, with a few more details what an implementer would have to look out for. Will extend with more details if necessary.

maxmitti commented 9 months ago

I just tried on Windows 10 and it works.

fritzw commented 9 months ago

I don't have a Windows 10 PC ready to cross-check, but I guess they changed something in Windows 11 then. When I disable Xidi and close Steam, my controller does not interact with Clonk at all, neither in the main menu, nor in the controller configuration menu, nor in-game.

Still, the other more important points remain.

maxmitti commented 9 months ago

I just fully realized that you want to split Jump from Up for Clonk controls. That seems indeed very difficult, also due to scripts.

fritzw commented 9 months ago

Oh, it's still there in Windows 11, but was not active by default for me. In the legacy control panel, there is an entry called "Set up USB game controllers", where you can "Select the device you want to use with older programs" after clicking "Advanced":

image

fritzw commented 9 months ago

I just fully realized that you want to split Jump from Up for Clonk controls. That seems indeed very difficult, also due to scripts.

Ah. Well, yes that's what I feared.

fritzw commented 9 months ago

I guess that separating the menu controls from the Clonk controls is the more important part from a usability standpoint, though. Having "Jump" and "Up" on the same key is occasionally annoying, but not being able to "Confirm" menu entries using "A" is a major problem in my opinion. That works against years of trained muscle memory.

maxmitti commented 9 months ago

I just fully realized that you want to split Jump from Up for Clonk controls. That seems indeed very difficult, also due to scripts.

Although, I thought about it more and it could actually work out decently for most packs. Jump is only the default action for Up. So it should be easily possible to inhibit that or trigger it separately.

maxmitti commented 9 months ago

I guess that separating the menu controls from the Clonk controls is the more important part from a usability standpoint, though. Having "Jump" and "Up" on the same key is occasionally annoying, but not being able to "Confirm" menu entries using "A" is a major problem in my opinion. That works against years of trained muscle memory.

Would it help to map Throw to two buttons? The one you use for controlling the clonk and also “A”. This is already possible with Extra.c4g/KeyConfig.txt:

[Keys]
Joy1Btn4=button1,button2

1 is for Gamepad 1 and 4 should be Throw (these + 1). button1 and button2 are parsed like this, which should be the same as displayed in the gamepad options.

You can also map to all the other controls found here.

fritzw commented 9 months ago

Would it help to map Throw to two buttons? The one you use for controlling the clonk and also “A”.

Not really, because the conventional mapping for button A in most games is "Jump". If I understand you correctly, this would not be possible with this suggestion, right? The minimal solution to my "problem" would be to map A to Thrown only while controlling a menu, and not a Clonk or other object.

maxmitti commented 9 months ago

Oh, right. What about Dig? Do you also have already a mapping for “B”?

fritzw commented 9 months ago

I have "Dig" mapped on B, which is fine because "Dig" also means "Cancel" in menus, and it is also the upper right key, just as in the default Clonk keyboard controls (for the same reason, I have "Throw" on X).

I mean, there are probably people out there who would map "Dig" to a different key but keep "Cancel" on B. So that option would be nice to have, but not as important as the other issue with A and Jump/Confirm.

fritzw commented 7 months ago

Okay, progress report: I've got a crude proof of concept working, that just changes the existing C4GamePad class to call the Xinput API instead of the WinMM joystick API. But this is actually worse than the previous workaround using Xidi, because Xidi allows you to map two buttons to the same action (e.g. having "Up/Jump" on both "A" and "Left stick up").

In order to make this viable, we would need a different input configuration mode, where you can select the controller buttons and then assign them actions. Possibly two different actions for "Menu Mode" and "Game Mode".

I guess a better approach is to first tackle the problem of having possibly different buttons for menu control and clonk/game control.

fritzw commented 7 months ago

Okay, here are some possible avenues of attack. Writing them up, just to help myself think about which one might be feasible.

  1. C4Menu::ConvertCom converts normal commands like "Dig" to menu commands like "Close".
    • One approach would be to give this function an additional player argument, so that it can look up the player's controls from the config, and adjust the conversion process accordingly.
    • C4ConfigGamepad would have to be expanded to store the Command-to-MenuCommand mappings, unless menu control is to be hardcoded for Xinput controllers to the conventional standard A=confirm, B=cancel.
      • A hardcoded menu controls for Xinput controllers sounds like a sensible thing, because all modern games do this for menu controls. But then all other gamepad users (and keyboard users) don't have the option to have different controls for menus and clonks.
    • C4Menu::DrawElement would have to use the same mapping as C4Menu::ConvertCom in reverse to pass the correct key to DrawCommandKey.
    • Probably the least invasive option, as it leaves all other input handling logic untouched.
  2. In C4Game::InitKeyboard the input keys for all 12 (C4MaxKey) possible commands for each input method are registered.
    • One could register the same button (e.g. "A") twice to emit both "CON_Up" and a new control "CON_MenuEnter".
    • However increasing C4MaxKey to add additional key definitions like CON_MenuEnter seems prone to unexpected bugs because the value of C4MaxKey is used in multiple places, and is sometimes implicitly assumed to be 12.
    • Also, C4Game::LocalPlayerControl would still somehow know to avoid calling ConvertCom on the COM_Up command, so the same mapping logic from option 1 would still be needed in addition to this.
    • Emitting two commands for one key press and then throwing one away seems prone to unintended double inputs, when option 1 does the same thing with just one event, and using less code changes.
  3. In C4Game::InitKeyboard, keys are registered with a scope in which they function, like KEYSCOPE_Control for in-game player inputs.
    • An idea might be to add a new KEYSCOPE_ControlMenu to register the same gamepad button twice with these two different scopes, and then filter which one to use based on whether the player has a menu open.
    • However, this option seems bound to fail because the scope is only checked in C4KeyboardInput::DoInput where this information is not available, and the scope is not passed down to the event handlers.
fritzw commented 7 months ago

So, I've got a basic prototype of option 1, including the forward and reverse mapping.

However, increasing C4MaxKey by at least 3 (for the 3 menu actions) is probably inevitable, because otherwise it is very messy to map menu actions to a button which is not one of the 12 default buttons. Because only those generate player control commands that can then be converted by C4Menu::ConvertCom, so additional buttons would have to be registered separately in C4Game::InitKeyboard. And in C4Game::LocalControlKey we would need separate CON_Menu* constants to idenfiy these keys.

Suddenly, Option 2 (registering buttons twice and throwing away the normal action while in a menu) looks like it might actually be the cleaner option.

Also, none of this currently handles the case where two buttons can trigger the same action like having both "A" and "Left Stick Up" for "Jump".

fritzw commented 7 months ago

Sooo.... I've got a working prototype that does everything I need, but likely still has some undiscovered bugs. There's a PR at #118 in case someone wants to have a look. Here's how it looks right now:

Object controls: image

Menu controls: image

fritzw commented 7 months ago

Note: Swimming with a joystick feels weird, because the swim direction depends on the time for which a direction key is held, which makes sense for keyboard controls and allows for fine grained direction control with fast key taps.

With a joystick however, you'd expect the swim direction to instantly reflect the direction you move the stick. Even if it was quantized to 8 directions it would feel more natural than the current behavior, where you have to quickly yank the stick around to get fine grained direction control. But I'm not sure how easy this would be to change.

Also, stick inputs should not be processed per-axis, but as a pair of axes, by interpreting the angle and deflection of the sick. The current implementation leads to a square activation window for the stick directions, which makes diagonal controls more difficult in my opinion. Also, some form of hysteresis would probably be nice to avoid accidental "double activations" when a stick is close to a diagonal direction and the value jiggles around the threshold.