Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic gamepad support and button remapping #35

Open
wants to merge 11 commits into
base: trunk
Choose a base branch
from

Conversation

techknight
Copy link
Contributor

@techknight techknight commented Dec 4, 2023

Summary

Gamepad support version 1! Players can use their gamepads to fly the plane during gameplay, and remap buttons in the options screen.

In Xbox terms, these default controls have been set up:

  • X: Fire machinegun
  • Y: Flip
  • A: Drop bomb
  • LB/RB: Decelerate/Accelerate
  • Menu: Go home

Compatibility

These setups have been tested as working in gameplay and in remapping:

  • Xbox One gamepad (Bluetooth) on Windows, MacOS, and Linux
  • Xbox Elite v2 gamepad (Xbox Wireless protocol) on Windows
  • 8bitdo Micro gamepad (Bluetooth, Mode D, d-pad) on Linux

Implementation

gamepad.c and gamepad.h have been added to handle most gamepad input during gameplay.

The most recent input device is tracked, to keep button handling from interfering with keyboard handling.

Gamepad Bindings

The options screen now supports remapping gamepad buttons:

New options

Gamepad Bindings with Names

Limitations

  • In this first version, only digital input is supported, no analog sticks or triggers
  • A limited number of gamepads have been tested

Future versions

  • Analog inputs could be supported, but it will require more complex input handling and a new way of saving configuration values
  • Navigating menus using the gamepad will require discussion, because it would radically change how menus are implemented

Copy link
Owner

@fragglet fragglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this!

src/gamepad.h Outdated
void Gamepad_Update(void);
void Gamepad_CheckState(void);

extern int btnbindings[NUM_KEYS];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NUM_KEYS is the number of keyboard bindings. You'll want to define a new one, eg. NUM_GAMEPAD_BUTTONS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

src/swconf.h Outdated
} type;
union {
void *v;
bool *b;
int *i;
int *btn;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be unused. I think you're doing the right thing by just using .i, but you can get rid of this field.

Copy link
Contributor Author

@techknight techknight Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up keeping this for the moment: there's two spots in drawmenu() and runmenu() where we need to know whether we're in the keyboard menu, or in the gamepad menu, in order to do the correct action. (ie checking for CONF_KEY vs CONF_BTN)

I did play around with another way where CONF_BTN was completely removed, where it was just like: if (!strcmp(title, "OPTIONS > GAMEPAD BINDINGS")) { 😅, but maybe I could create a new MENU_TYPE that gets passed to runmenu when it gets executed? That might be the cleanest, and get rid of using CONF_BTN altogether

src/sdl/swconf.c Outdated
sprintf(btnString, "%d", *opt->value.i); // Convert integer to string
swputs(btnString);
break;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the {} braces here; they are misleading.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

src/Makefile.am Outdated
pcsound.h \
std.h \
swasynio.c swasynio.h \
swauto.c swauto.h \
swcollsn.c swcollsn.h \
swconf.c swconf.h \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move swconf.c and swinit.c back to their previous location, they don't belong in the sdl/ subdirectory.

I understand why you had to do this: the new feature requires using new SDL functions. But the correct way to do this is to create accessor functions to access them. For example, we have Vid_GetKey() that is used by the configuration code to read a keypress. You need a similar function that your change_btn_binding function can call to read a gamepad press.

In case it's not clear: the code in src/ is the "core code" of the game. The idea is that if someone wants to port the game to a new system where SDL isn't available, they should just be able to reimplement the files in src/sdl/ . Think of it as though there were multiple other directories alongside src/sdl/ - we wouldn't want to contain duplicate implementations of the initialization and configuration code for every one of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad - I should've asked in Discord for some more context. This is all moved back and straightened out.


void Gamepad_Update(void) {
if (!gamepad_initted || gamepad == NULL) {
Gamepad_Init();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to try to initialize again every time the function is called. Let's just return here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (!gamepad_initted) {
return;
}
// Close the gamepad
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try to be more consistent with your formatting? Not just talking about this one function but throughout the PR. You can see here that it's kind of a mess because you mixed up spaces and tabs.

The coding style for the project is to use tabs for indentation. If you configure your editor to not expand tabs to spaces then you'll probably get the right thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, sorry about that, I've fixed my settings there

@techknight techknight requested a review from fragglet December 6, 2023 03:08
@techknight
Copy link
Contributor Author

In the changes today, I've also added support for looking up the button names so that they look nice now in the button config menu ✨

while (btn == -1) {
if (SDL_WaitEventTimeout(&event, 10)) {
if (event.type == SDL_CONTROLLERBUTTONDOWN) {
btn = event.cbutton.button;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Justreturn event.cbutton.button; and you can eliminate the btn variable.

{
SDL_Event event;

while (SDL_PollEvent(&event)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this means that there are now two event loops: the one in the gamepad code and the one in the video code. This will mean that some events get discarded mistakenly. You need to use a single combined event loop.

{
if (SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) {
printf("Gamepad could not initialize! SDL_Error: %s\n", SDL_GetError());
} else {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else if (...

btn = event.cbutton.button;
break;
} else if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_ESCAPE) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can combine the two if statements with an &&

last_input_gamepad = input;
}

int isGamepadInitted(void) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get rid of this function; just add a check to Gamepad_GetGameBtns that returns 0 if !gamepad_initted.

int tictime;

/* generate a new move command and save it */

multkey = Vid_GetGameKeys();
if(isGamepadInitted()) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason you can't just:

multkey = Vid_GetGameKeys() | Gamepad_GetGameBtns();

?

Then I think you can get rid of all the "LastInputGamepad" stuff entirely.

@@ -536,12 +533,12 @@ void Vid_SetVideoPalette(int palette)

const char* Vid_GetVideoPaletteName(int palette)
{
return VideoPalettes[palette].name;
return VideoPalettes[palette].name;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too much indentation. Side note, can you try to avoid including unrelated changes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants