Control Remapping Buttons and Axis

Hi all. I am working on a game and trying to implement customizable controls for a controller. However, I am confused on how to make SDL_CONTROLLER_AXIS_TRIGGERLEFT and SDL_CONTROLLER_AXIS_TRIGGERRIGHT remappable with the buttons. For my game, I map each game action (such as a jump) to an input state. That way the user can change jump to whatever button they want. This is causing me a headache when trying to involve the triggers however since they are axis and not buttons. Buttons and Axis also share enum values so 4 is SDL_CONTROLLER_AXIS_TRIGGERLEFT or SDL_CONTROLLER_BUTTON_BACK. I’m working in plain c btw. I’ve been looking at the documentation, but I might be missing something.

If anyone has any ideas I’d appreciate it. Thanks!

I don’t know the solution of your program, but I have a question — why did you choose SDL’s gamepads API and built-in mapping system instead of joysticks API and your own input mapper? What’s the reason? What advantages does a built-in SDL mapper have over something you can write yourself?

Not sure I understand the question. I am trying to implement remappable controls to this project. You can see the current code here: Tomb1Main/s_input.c at develop · rr-/Tomb1Main · GitHub

Currently the controller buttons are hard coded and can’t be changed.

I asked why you chose the SDL_GameController API and its functions instead of using the SDL_Joystick functions which you can use however you like. But it does not matter.

To be honest, if you want a powerful input mapper, with the possibility of its rich configuration, you should write it yourself. Don’t rely entirely on the gamepad API, because it won’t do you any good — the whole remapping system needs to be written anyway, so it’s better to do it on your terms.

I don’t know exactly what remapping functionality you are interested in, but I will write briefly how I approached this topic in the germ of my current game project.


ASSUMPTIONS

  • There is a set of supported players — one in single mode, two in co-op mode.
  • Each player supports a fixed set of actions — there are a 14 of them in total, some simple (two-state, like jump), and some complex (like walk, operated with one or more manipulators).
  • Each player can assign to himself any number of available input devices of any type (mouse, keyboard and gamepads), but devices cannot be shared by several players.
  • Any manipulator (or several) of any input device assigned to the player can be assigned to each action (with some restrictions).

ACTION

The action object has several integer/enum fields, including action ID, device type (mouse, keyboard, gamepad), device ID (e.g. gamepad index) and operation mode number (e.g. key scancode, mouse button, gamepad axis, etc.). In addition to them, there are also arrays with four values — types of manipulators (e.g. gamepad button), scancodes of manipulators (e.g. gamepad button number) and directions (needed to map two-state actions e.g. to the D-Pad, axis or stick).

As you can see, the action object contains all the necessary information so that it can be assigned to any device and its manipulator. In the case of a two-state action, it is enough to enter the device type, its ID, mode, keypad type and its scancode into the object. In the case of a complex action like walk, it can be performed with:

  • one manipulator — using gamepad stick,
  • four manipulators — using four keys or four gamepad buttons.

That is why the scancodes are contained in an array with four cells, because a given action can be performed with up to four manipulators. If the action is to use an multidirectional manipulator (e.g. analog stick or axis), you must also define the direction.

The action can also be performed using two manipulators, but the principle is the same.

PLAYER

It contains a list of objects of all actions. There are as many player objects as the game supports at most, stored also in a separate list.

MAPPED DEVICES

This object stores information about which player is assigned to which input device. For mouse and keyboard, it only stores the assigned player ID (SDL does not support multiple mice and multiple keyboards), and for gamepads, it stores the player ID and device information (handle, checksum and instance ID).

For gamepads, the information in this object is only modified when assigning devices to players (or assigning “to no one”) — connecting and disconnecting gamepads only updates the handles and instance IDs. After all, disconnecting the gamepad from the computer cannot remove the mapping.

ASSIGNING DEVICES TO PLAYERS

Which player is assigned to which device is listed in the devices object. Binding a player to a device, changing the player ID assigned to a device, or disconnecting a device from a player is just a modification of the numeric ID. This is for mouse and keyboard.

A separate case is the assignment of gamepads — attaching a gamepad to a given player means creating an object with its data (handle, instance ID, etc. and the ID of the player) and adding it to the list. Detaching the gamepad from the player means removing such an object from the list of mapped devices.

READING PLAYER INPUT

If the mapper wants to check if a given player performs a given action (somewhere inside the game logic update code), it performs a series of actions. In brief:

  • Retrieves the given player object from the list.
  • Retrieves the given action object from this player’s action list.
  • Gets the type of input device assigned to the action (mouse, keyboard or gamepad) and on this basis distinguishes what type of device to check.
  • Gets the action handling mode (e.g. simple, pair, quad, hat, stick) and on this basis distinguishes how to check if it is executed.
  • Retrieves the type of the manipulator (key, button, axis, stick etc.), based on which it makes another distinction of handling.
  • Finally, it gets the scancode of the manipulator (or several) and optionally directions, reads the values of specific manipulators and determines whether the action is performed or not. When a gamepad manipulator is assigned to an action, to be able to read its data, it first looks at the list of devices and checks if the gamepad is connected — if so, its handle is not null.

Note that I store the data of the mouse, keyboard and all gamepads in structures on the application side, so that I always have access to them, without polling devices using SDL functions or “hot” processing of SDL events.

To change the mapping of a given action, it is enough to modify the device and manipulator data in the player’s action object. The polling must be programmed so that the game checks which devices are assigned to a given player (in the case of gamepads, additionally whether they are connected), then which types of manipulators can be assigned to a given action (because, for example, walking with the mouse wheel is pointless), and finally check the status of all manipulators of a given type in a given device and if any of them is used (e.g. pressed or tilted enough), then data about it is returned, which should be entered into the action object.


SUMMARY

What I described above may seem complicated, but it is a relatively simple system. It requires writing more code, but it allows you to do what you need, i.e. map the input ~freely.

Of course, I didn’t describe everything, because there are thousands of additional things, such as returning additional information, e.g. for the walk action, such as normalized magnitude, angle and direction; virtual stick support with relative mouse mode; storing the values of all manipulators of all devices in state objects that remember data from the previous and current frame of the game, etc. etc.

However, what I described above is the bare minimum, thanks to which you will be able to write a mapper that gives the player a choice and allows you to assign manipulators to different actions, regardless of the source device and manipulator type.