improved gamepad support using SDL GameController API

still WIP, but it's better than it was

todo (maybe):
    - handle hotplugging
    - enable gamepad support by default
    - treat analog triggers as buttons
    - better way of saving which device to use to the config, the index
    is virtually useless
This commit is contained in:
Andrei "Akari" Alexeyev 2017-07-15 06:22:56 +03:00
parent 70933bcd4e
commit 57a138c1d1
No known key found for this signature in database
GPG key ID: 048C3D2A5648B785
5 changed files with 234 additions and 119 deletions

View file

@ -16,7 +16,7 @@ static bool config_initialized = false;
CONFIGDEFS_EXPORT ConfigEntry configdefs[] = {
#define CONFIGDEF(type,entryname,default,ufield) {type, entryname, {.ufield = default}, NULL},
#define CONFIGDEF_KEYBINDING(id,entryname,default) CONFIGDEF(CONFIG_TYPE_KEYBINDING, entryname, default, i)
#define CONFIGDEF_GPKEYBINDING(id,entryname,default) CONFIGDEF(CONFIG_TYPE_INT, entryname, default, i)
#define CONFIGDEF_GPKEYBINDING(id,entryname,default) CONFIGDEF(CONFIG_TYPE_GPKEYBINDING, entryname, default, i)
#define CONFIGDEF_INT(id,entryname,default) CONFIGDEF(CONFIG_TYPE_INT, entryname, default, i)
#define CONFIGDEF_FLOAT(id,entryname,default) CONFIGDEF(CONFIG_TYPE_FLOAT, entryname, default, f)
#define CONFIGDEF_STRING(id,entryname,default) CONFIGDEF(CONFIG_TYPE_STRING, entryname, NULL, s)
@ -156,6 +156,7 @@ static void config_set_val(ConfigIndex idx, ConfigValue v) {
switch(e->type) {
case CONFIG_TYPE_INT:
case CONFIG_TYPE_KEYBINDING:
case CONFIG_TYPE_GPKEYBINDING:
difference = e->val.i != v.i;
break;
@ -184,6 +185,7 @@ static void config_set_val(ConfigIndex idx, ConfigValue v) {
switch(e->type) {
case CONFIG_TYPE_INT:
case CONFIG_TYPE_KEYBINDING:
case CONFIG_TYPE_GPKEYBINDING:
e->val.i = v.i;
PRINTVAL(i)
break;
@ -290,6 +292,10 @@ void config_save(void) {
SDL_RWprintf(out, "%s = %s\n", e->name, SDL_GetScancodeName(e->val.i));
break;
case CONFIG_TYPE_GPKEYBINDING:
SDL_RWprintf(out, "%s = %s\n", e->name, SDL_GameControllerGetStringForButton(e->val.i));
break;
case CONFIG_TYPE_STRING:
SDL_RWprintf(out, "%s = %s\n", e->name, e->val.s);
break;
@ -344,6 +350,18 @@ static void config_set(const char *key, const char *val, void *data) {
break;
}
case CONFIG_TYPE_GPKEYBINDING: {
SDL_GameControllerButton btn = SDL_GameControllerGetButtonFromString(val);
if(btn == SDL_CONTROLLER_BUTTON_INVALID) {
log_warn("Unknown gamepad key '%s'", val);
} else {
e->val.i = btn;
}
break;
}
case CONFIG_TYPE_STRING:
stralloc(&e->val.s, val);
break;

View file

@ -56,15 +56,14 @@
#define GPKEYDEFS \
CONFIGDEF_GPKEYBINDING(KEY_UP, "gamepad_key_up", -1) \
CONFIGDEF_GPKEYBINDING(KEY_DOWN, "gamepad_key_down", -1) \
CONFIGDEF_GPKEYBINDING(KEY_LEFT, "gamepad_key_left", -1) \
CONFIGDEF_GPKEYBINDING(KEY_RIGHT, "gamepad_key_right", -1) \
CONFIGDEF_GPKEYBINDING(KEY_FOCUS, "gamepad_key_focus", 0) \
CONFIGDEF_GPKEYBINDING(KEY_SHOT, "gamepad_key_shot", 1) \
CONFIGDEF_GPKEYBINDING(KEY_BOMB, "gamepad_key_bomb", 2) \
CONFIGDEF_GPKEYBINDING(KEY_SKIP, "gamepad_key_skip", 3) \
CONFIGDEF_GPKEYBINDING(KEY_PAUSE, "gamepad_key_pause", 4) \
CONFIGDEF_GPKEYBINDING(KEY_UP, "gamepad_key_up", SDL_CONTROLLER_BUTTON_DPAD_UP) \
CONFIGDEF_GPKEYBINDING(KEY_DOWN, "gamepad_key_down", SDL_CONTROLLER_BUTTON_DPAD_DOWN) \
CONFIGDEF_GPKEYBINDING(KEY_LEFT, "gamepad_key_left", SDL_CONTROLLER_BUTTON_DPAD_LEFT) \
CONFIGDEF_GPKEYBINDING(KEY_RIGHT, "gamepad_key_right", SDL_CONTROLLER_BUTTON_DPAD_RIGHT) \
CONFIGDEF_GPKEYBINDING(KEY_FOCUS, "gamepad_key_focus", SDL_CONTROLLER_BUTTON_X) \
CONFIGDEF_GPKEYBINDING(KEY_SHOT, "gamepad_key_shot", SDL_CONTROLLER_BUTTON_A) \
CONFIGDEF_GPKEYBINDING(KEY_BOMB, "gamepad_key_bomb", SDL_CONTROLLER_BUTTON_Y) \
CONFIGDEF_GPKEYBINDING(KEY_SKIP, "gamepad_key_skip", SDL_CONTROLLER_BUTTON_B) \
#define CONFIGDEFS \
@ -153,6 +152,7 @@ typedef enum ConfigEntryType {
CONFIG_TYPE_INT,
CONFIG_TYPE_STRING,
CONFIG_TYPE_KEYBINDING,
CONFIG_TYPE_GPKEYBINDING,
CONFIG_TYPE_FLOAT
} ConfigEntryType;

View file

@ -13,66 +13,98 @@
static struct {
int initialized;
SDL_Joystick *device;
SDL_GameController *device;
SDL_JoystickID instance;
signed char *axis;
struct {
int *id_map;
size_t count;
} devices;
} gamepad;
static void gamepad_update_device_list(void) {
int cnt = SDL_NumJoysticks();
log_info("Updating gamepad devices list");
free(gamepad.devices.id_map);
memset(&gamepad.devices, 0, sizeof(gamepad.devices));
if(!cnt) {
return;
}
int *idmap_ptr = gamepad.devices.id_map = malloc(sizeof(int) * cnt);
for(int i = 0; i < cnt; ++i) {
if(!SDL_IsGameController(i)) {
continue;
}
*idmap_ptr = i;
log_info("Gamepad device #%"PRIuMAX": %s", (uintmax_t)(idmap_ptr - gamepad.devices.id_map), SDL_GameControllerNameForIndex(i));
++idmap_ptr;
}
gamepad.devices.count = (uintptr_t)(idmap_ptr - gamepad.devices.id_map);
}
void gamepad_init(void) {
memset(&gamepad, 0, sizeof(gamepad));
if(!config_get_int(CONFIG_GAMEPAD_ENABLED))
if(!config_get_int(CONFIG_GAMEPAD_ENABLED) || gamepad.initialized) {
return;
}
if(gamepad.initialized)
return;
if(SDL_InitSubSystem(SDL_INIT_JOYSTICK) < 0) {
if(SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
log_warn("SDL_InitSubSystem() failed: %s", SDL_GetError());
return;
}
int i, cnt = gamepad_devicecount();
log_info("Found %i devices", cnt);
for(i = 0; i < cnt; ++i)
log_info("Device #%i: %s", i, SDL_JoystickNameForIndex(i));
gamepad_update_device_list();
int dev = config_get_int(CONFIG_GAMEPAD_DEVICE);
if(dev < 0 || dev >= cnt) {
if(dev < 0 || dev >= gamepad.devices.count) {
log_warn("Device #%i is not available", dev);
gamepad_shutdown();
return;
}
gamepad.device = SDL_JoystickOpen(dev);
gamepad.device = SDL_GameControllerOpen(dev);
if(!gamepad.device) {
log_warn("Failed to open device %i: %s", dev, SDL_GetError());
gamepad_shutdown();
return;
}
gamepad.axis = malloc(SDL_JoystickNumAxes(gamepad.device));
SDL_Joystick *joy = SDL_GameControllerGetJoystick(gamepad.device);
gamepad.instance = SDL_JoystickInstanceID(joy);
gamepad.axis = malloc(max(SDL_JoystickNumAxes(joy), 1));
log_info("Using device #%i: %s", dev, gamepad_devicename(dev));
SDL_JoystickEventState(SDL_ENABLE);
SDL_GameControllerEventState(SDL_ENABLE);
gamepad.initialized = 1;
}
void gamepad_shutdown(void) {
if(!gamepad.initialized)
if(!gamepad.initialized) {
return;
}
if(gamepad.initialized != 2)
if(gamepad.initialized != 2) {
log_info("Disabled the gamepad subsystem");
}
if(gamepad.device)
SDL_JoystickClose(gamepad.device);
if(gamepad.device) {
SDL_GameControllerClose(gamepad.device);
}
free(gamepad.axis);
gamepad.axis = NULL;
free(gamepad.devices.id_map);
SDL_JoystickEventState(SDL_IGNORE);
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
gamepad.initialized = 0;
SDL_GameControllerEventState(SDL_IGNORE);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
memset(&gamepad, 0, sizeof(gamepad));
}
void gamepad_restart(void) {
@ -80,7 +112,7 @@ void gamepad_restart(void) {
gamepad_init();
}
int gamepad_axis2gamekey(int id, int val) {
int gamepad_axis2gamekey(SDL_GameControllerAxis id, int val) {
val *= sign(gamepad_axis_sens(id));
if(!val) {
@ -98,17 +130,17 @@ int gamepad_axis2gamekey(int id, int val) {
return -1;
}
int gamepad_axis2menuevt(int id, int val) {
if(id == config_get_int(CONFIG_GAMEPAD_AXIS_LR))
int gamepad_axis2menuevt(SDL_GameControllerAxis id, int val) {
if(id == SDL_CONTROLLER_AXIS_LEFTX || id == SDL_CONTROLLER_AXIS_RIGHTX)
return val == AXISVAL_LEFT ? E_CursorLeft : E_CursorRight;
if(id == config_get_int(CONFIG_GAMEPAD_AXIS_UD))
if(id == SDL_CONTROLLER_AXIS_LEFTY || id == SDL_CONTROLLER_AXIS_RIGHTY)
return val == AXISVAL_UP ? E_CursorUp : E_CursorDown;
return -1;
}
int gamepad_axis2gameevt(int id) {
int gamepad_axis2gameevt(SDL_GameControllerAxis id) {
if(id == config_get_int(CONFIG_GAMEPAD_AXIS_LR))
return E_PlrAxisLR;
@ -118,7 +150,7 @@ int gamepad_axis2gameevt(int id) {
return -1;
}
float gamepad_axis_sens(int id) {
float gamepad_axis_sens(SDL_GameControllerAxis id) {
if(id == config_get_int(CONFIG_GAMEPAD_AXIS_LR))
return config_get_float(CONFIG_GAMEPAD_AXIS_LR_SENS);
@ -128,7 +160,7 @@ float gamepad_axis_sens(int id) {
return 1.0;
}
void gamepad_axis(int id, int raw, EventHandler handler, EventFlags flags, void *arg) {
void gamepad_axis(SDL_GameControllerAxis id, int raw, EventHandler handler, EventFlags flags, void *arg) {
signed char *a = gamepad.axis;
signed char val = AXISVAL(raw);
bool free = config_get_int(CONFIG_GAMEPAD_AXIS_FREE);
@ -162,14 +194,16 @@ void gamepad_axis(int id, int raw, EventHandler handler, EventFlags flags, void
if(game && !free) {
int key = gamepad_axis2gamekey(id, val);
if(key >= 0)
if(key >= 0) {
handler(E_PlrKeyDown, key, arg);
}
}
if(menu) {
int evt = gamepad_axis2menuevt(id, val);
if(evt >= 0)
if(evt >= 0) {
handler(evt, 0, arg);
}
}
}
} else { // simulate release
@ -187,7 +221,7 @@ void gamepad_axis(int id, int raw, EventHandler handler, EventFlags flags, void
}
}
void gamepad_button(int button, int state, EventHandler handler, EventFlags flags, void *arg) {
void gamepad_button(SDL_GameControllerButton button, int state, EventHandler handler, EventFlags flags, void *arg) {
int menu = flags & EF_Menu;
int game = flags & EF_Game;
int gpad = flags & EF_Gamepad;
@ -195,36 +229,46 @@ void gamepad_button(int button, int state, EventHandler handler, EventFlags flag
int gpkey = config_gamepad_key_from_gamepad_button(button);
int key = config_key_from_gamepad_key(gpkey);
//printf("button: %i %i\n", button, state);
//printf("gpkey: %i\n", gpkey);
//printf("key: %i\n", key);
if(state == SDL_PRESSED) {
if(game) {
if(gpkey == GAMEPAD_KEY_PAUSE) {
handler(E_Pause, 0, arg);
} else if(key >= 0) {
handler(E_PlrKeyDown, key, arg);
}
if(game) switch(button) {
case SDL_CONTROLLER_BUTTON_START:
case SDL_CONTROLLER_BUTTON_BACK:
handler(E_Pause, 0, arg); break;
default:
if(key >= 0) {
handler(E_PlrKeyDown, key, arg);
} break;
}
if(menu) switch(key) {
case KEY_UP: handler(E_CursorUp, 0, arg); break;
case KEY_DOWN: handler(E_CursorDown, 0, arg); break;
case KEY_LEFT: handler(E_CursorLeft, 0, arg); break;
case KEY_RIGHT: handler(E_CursorRight, 0, arg); break;
case KEY_FOCUS: handler(E_MenuAbort, 0, arg); break;
case KEY_SHOT: handler(E_MenuAccept, 0, arg); break;
if(menu) switch(button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP: handler(E_CursorUp, 0, arg); break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: handler(E_CursorDown, 0, arg); break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT: handler(E_CursorLeft, 0, arg); break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: handler(E_CursorRight, 0, arg); break;
case SDL_CONTROLLER_BUTTON_A:
case SDL_CONTROLLER_BUTTON_START:
handler(E_MenuAccept, 0, arg); break;
case SDL_CONTROLLER_BUTTON_B:
case SDL_CONTROLLER_BUTTON_BACK:
handler(E_MenuAbort, 0, arg); break;
default: break;
}
if(gpad)
if(gpad) {
handler(E_GamepadKeyDown, button, arg);
}
} else {
if(game && key >= 0)
if(game && key >= 0) {
handler(E_PlrKeyUp, key, arg);
}
if(gpad)
if(gpad) {
handler(E_GamepadKeyUp, button, arg);
}
}
}
@ -238,36 +282,45 @@ void gamepad_event(SDL_Event *event, EventHandler handler, EventFlags flags, voi
int minval = clamp(deadzone, 0, 1) * GAMEPAD_AXIS_MAX;
switch(event->type) {
case SDL_JOYAXISMOTION:
val = event->jaxis.value;
vsign = sign(val);
val = abs(val);
case SDL_CONTROLLERAXISMOTION:
if(event->caxis.which == gamepad.instance) {
val = event->caxis.value;
vsign = sign(val);
val = abs(val);
if(val < minval) {
val = 0;
} else {
val = vsign * clamp((val - minval) / (1.0 - deadzone), 0, GAMEPAD_AXIS_MAX);
if(val < minval) {
val = 0;
} else {
val = vsign * clamp((val - minval) / (1.0 - deadzone), 0, GAMEPAD_AXIS_MAX);
}
gamepad_axis(event->caxis.axis, val, handler, flags, arg);
}
gamepad_axis(event->jaxis.axis, val, handler, flags, arg);
break;
case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP:
gamepad_button(event->jbutton.button, event->jbutton.state, handler, flags, arg);
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
if(event->cbutton.which == gamepad.instance) {
gamepad_button(event->cbutton.button, event->cbutton.state, handler, flags, arg);
}
break;
}
}
int gamepad_devicecount(void) {
return SDL_NumJoysticks();
return gamepad.devices.count;
}
char* gamepad_devicename(int id) {
return (char*)SDL_JoystickNameForIndex(id);
const char* gamepad_devicename(int id) {
if(id < 0 || id >= gamepad.devices.count) {
return NULL;
}
return SDL_GameControllerNameForIndex(gamepad.devices.id_map[id]);
}
bool gamepad_buttonpressed(int btn) {
return SDL_JoystickGetButton(gamepad.device, btn);
bool gamepad_buttonpressed(SDL_GameControllerButton btn) {
return SDL_GameControllerGetButton(gamepad.device, btn);
}
bool gamepad_gamekeypressed(KeyIndex key) {
@ -287,18 +340,65 @@ bool gamepad_gamekeypressed(KeyIndex key) {
}
void gamepad_init_bare(void) {
if(gamepad.initialized)
if(gamepad.initialized) {
return;
}
if(SDL_InitSubSystem(SDL_INIT_JOYSTICK) < 0) {
if(SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
log_warn("SDL_InitSubSystem() failed: %s", SDL_GetError());
return;
}
gamepad_update_device_list();
gamepad.initialized = 2;
}
void gamepad_shutdown_bare(void) {
if(gamepad.initialized == 2)
if(gamepad.initialized == 2) {
gamepad_shutdown();
}
}
const char* gamepad_button_name(SDL_GameControllerButton btn) {
static const char *const map[] = {
"A",
"B",
"X",
"Y",
"Back",
"Guide",
"Start",
"Left Stick",
"Right Stick",
"Left Bumper",
"Right Bumper",
"Up",
"Down",
"Left",
"Right",
};
if(btn > SDL_CONTROLLER_BUTTON_INVALID && btn < SDL_CONTROLLER_BUTTON_MAX) {
return map[btn];
}
return "Unknown";
}
const char* gamepad_axis_name(SDL_GameControllerAxis axis) {
static const char *const map[] = {
"Left X",
"Left Y",
"Right X",
"Right Y",
"Left Trigger",
"Right Trigger",
};
if(axis > SDL_CONTROLLER_AXIS_INVALID && axis < SDL_CONTROLLER_AXIS_MAX) {
return map[axis];
}
return "Unknown";
}

View file

@ -19,12 +19,15 @@ void gamepad_shutdown(void);
void gamepad_restart(void);
int gamepad_devicecount(void);
float gamepad_axis_sens(int);
char* gamepad_devicename(int);
const char* gamepad_devicename(int);
void gamepad_event(SDL_Event*, EventHandler, EventFlags, void*);
bool gamepad_buttonpressed(int btn);
bool gamepad_gamekeypressed(KeyIndex key);
const char* gamepad_button_name(SDL_GameControllerButton btn);
const char* gamepad_axis_name(SDL_GameControllerAxis btn);
// shitty workaround for the options menu. Used to list devices while the gamepad subsystem is off.
// only initializes the SDL subsystem so you can use gamepad_devicecount/gamepad_devicename.
// if gamepad has been initialized already, these do nothing.

View file

@ -391,30 +391,6 @@ void options_sub_gamepad_controls(MenuData *parent, void *arg) {
create_options_menu_basic(m, "Gamepad Controls");
add_menu_entry(m, "Fire / Accept", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_SHOT)
);
add_menu_entry(m, "Focus / Abort", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_FOCUS)
);
add_menu_entry(m, "Bomb", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_BOMB)
);
add_menu_separator(m);
add_menu_entry(m, "Pause", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_PAUSE)
);
add_menu_entry(m, "Skip dialog", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_SKIP)
);
add_menu_separator(m);
add_menu_entry(m, "Move up", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_UP)
);
@ -431,6 +407,26 @@ void options_sub_gamepad_controls(MenuData *parent, void *arg) {
bind_gpbinding(CONFIG_GAMEPAD_KEY_RIGHT)
);
add_menu_separator(m);
add_menu_entry(m, "Fire", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_SHOT)
);
add_menu_entry(m, "Focus", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_FOCUS)
);
add_menu_entry(m, "Bomb", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_BOMB)
);
add_menu_separator(m);
add_menu_entry(m, "Skip dialog", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_SKIP)
);
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
@ -733,13 +729,13 @@ void draw_options_menu(MenuData *menu) {
if(bind->blockinput) {
glColor4f(0.5, 1, 0.5, 1);
draw_text(AL_Right, origin, 20*i,
is_axis ? "Move an axis to assign, press any button to cancel"
: "Press a button to assign, move an axis to cancel",
is_axis ? "Move an axis to assign, Back to cancel"
: "Press a button to assign, Back to cancel",
_fonts.standard);
} else if(config_get_int(bind->configentry) >= 0) {
char tmp[32];
snprintf(tmp, 32, is_axis ? "Axis %i" : "Button %i", config_get_int(bind->configentry) + 1);
draw_text(AL_Right, origin, 20*i, tmp, _fonts.standard);
int id = config_get_int(bind->configentry);
const char *name = (is_axis ? gamepad_axis_name(id) : gamepad_button_name(id));
draw_text(AL_Right, origin, 20*i, name, _fonts.standard);
} else {
draw_text(AL_Right, origin, 20*i, "Unbound", _fonts.standard);
}
@ -850,6 +846,9 @@ void bind_input_event(EventType type, int state, void *arg) {
if(b->type == BT_GamepadAxisBinding)
b->blockinput = false;
break;
} else if(scan == SDL_CONTROLLER_BUTTON_BACK || scan == SDL_CONTROLLER_BUTTON_START) {
b->blockinput = false;
break;
}
for(int i = CONFIG_GAMEPAD_KEY_FIRST; i <= CONFIG_GAMEPAD_KEY_LAST; ++i) {
@ -864,11 +863,6 @@ void bind_input_event(EventType type, int state, void *arg) {
}
case E_GamepadAxis: {
if(b->type == BT_GamepadKeyBinding) {
b->blockinput = false;
break;
}
if(b->type == BT_GamepadAxisBinding) {
if(b->configentry == CONFIG_GAMEPAD_AXIS_UD) {
if(config_get_int(CONFIG_GAMEPAD_AXIS_LR) == state) {