From b57445900fce6a97031e04ade098c90f9de33b10 Mon Sep 17 00:00:00 2001 From: Andrei Alexeyev Date: Tue, 11 Jul 2023 23:51:44 +0200 Subject: [PATCH] gamepad: overhaul analog stick handling * Use correctly scaled radial deadzones instead of naive per-axis deadzones * Option to adjust the "maximum zone" (upper deadzone) * Option to remap square input into circular. Unfortunately there's no nice way to detect which type the controller reports. We assume circular by default. * A more sensible sensitivity setting * Use a larger minimum deadzone when emulating key presses (e.g. in menus) * Adjusted key repeat to be less aggressive --- src/config.c | 6 ++ src/config.h | 14 ++- src/events.h | 1 + src/gamepad.c | 189 +++++++++++++++++++++------------------- src/gamepad.h | 2 +- src/menu/options.c | 29 +++--- src/player.c | 4 +- src/replay/demoplayer.c | 2 +- src/stage.c | 9 +- 9 files changed, 136 insertions(+), 120 deletions(-) diff --git a/src/config.c b/src/config.c index 9e383722..1ebfc4fb 100644 --- a/src/config.c +++ b/src/config.c @@ -418,6 +418,11 @@ static void config_upgrade_3(void) { config_set_int(CONFIG_MIXER_CHUNKSIZE, CONFIG_CHUNKSIZE_DEFAULT); } +static void config_upgrade_4(void) { + config_set_float(CONFIG_GAMEPAD_BTNREPEAT_DELAY, CONFIG_GAMEPAD_BTNREPEAT_DELAY_DEFAULT); + config_set_float(CONFIG_GAMEPAD_BTNREPEAT_INTERVAL, CONFIG_GAMEPAD_BTNREPEAT_INTERVAL_DEFAULT); +} + static ConfigUpgradeFunc config_upgrades[] = { /* To bump the config version and add an upgrade state, simply append an upgrade function to this array. @@ -430,6 +435,7 @@ static ConfigUpgradeFunc config_upgrades[] = { config_upgrade_1, config_upgrade_2, config_upgrade_3, + config_upgrade_4, }; static void config_apply_upgrades(int start) { diff --git a/src/config.h b/src/config.h index 1fd9a2fa..4b81459a 100644 --- a/src/config.h +++ b/src/config.h @@ -85,6 +85,9 @@ #define CONFIG_CHUNKSIZE_DEFAULT 512 +#define CONFIG_GAMEPAD_BTNREPEAT_DELAY_DEFAULT 0.5 +#define CONFIG_GAMEPAD_BTNREPEAT_INTERVAL_DEFAULT 0.04 + #define CONFIGDEFS \ /* @version must be on top. don't change its default value here, it does nothing. */ \ CONFIGDEF_INT (VERSION, "@version", 0) \ @@ -123,11 +126,14 @@ CONFIGDEF_INT (GAMEPAD_AXIS_UD, "gamepad_axis_ud", 1) \ CONFIGDEF_INT (GAMEPAD_AXIS_LR, "gamepad_axis_lr", 0) \ CONFIGDEF_INT (GAMEPAD_AXIS_FREE, "gamepad_axis_free", 1) \ - CONFIGDEF_FLOAT (GAMEPAD_AXIS_UD_SENS, "gamepad_axis_ud_free_sensitivity", 1.0) \ - CONFIGDEF_FLOAT (GAMEPAD_AXIS_LR_SENS, "gamepad_axis_lr_free_sensitivity", 1.0) \ + CONFIGDEF_INT (GAMEPAD_AXIS_SQUARECIRCLE, "gamepad_axis_square_to_circle", 0) \ + CONFIGDEF_FLOAT (GAMEPAD_AXIS_SENS, "gamepad_axis_sensitivity", 0.0) \ + CONFIGDEF_FLOAT (GAMEPAD_AXIS_UD_SENS, "gamepad_axis_ud_sensitivity", 0.0) \ + CONFIGDEF_FLOAT (GAMEPAD_AXIS_LR_SENS, "gamepad_axis_lr_sensitivity", 0.0) \ CONFIGDEF_FLOAT (GAMEPAD_AXIS_DEADZONE, "gamepad_axis_deadzone", 0.1) \ - CONFIGDEF_FLOAT (GAMEPAD_BTNREPEAT_DELAY, "gamepad_button_repeat_delay", 0.25) \ - CONFIGDEF_FLOAT (GAMEPAD_BTNREPEAT_INTERVAL,"gamepad_button_repeat_interval", 0.02) \ + CONFIGDEF_FLOAT (GAMEPAD_AXIS_MAXZONE, "gamepad_axis_maxzone", 0.9) \ + CONFIGDEF_FLOAT (GAMEPAD_BTNREPEAT_DELAY, "gamepad_button_repeat_delay", CONFIG_GAMEPAD_BTNREPEAT_DELAY_DEFAULT) \ + CONFIGDEF_FLOAT (GAMEPAD_BTNREPEAT_INTERVAL,"gamepad_button_repeat_interval", CONFIG_GAMEPAD_BTNREPEAT_INTERVAL_DEFAULT) \ GPKEYDEFS \ diff --git a/src/events.h b/src/events.h index 2c103398..c9c41553 100644 --- a/src/events.h +++ b/src/events.h @@ -40,6 +40,7 @@ typedef enum { TE_GAMEPAD_BUTTON_DOWN, TE_GAMEPAD_BUTTON_UP, TE_GAMEPAD_AXIS, + TE_GAMEPAD_AXIS_DIGITAL, TE_VIDEO_MODE_CHANGED, diff --git a/src/gamepad.c b/src/gamepad.c index cb2163d2..419976d5 100644 --- a/src/gamepad.c +++ b/src/gamepad.c @@ -16,7 +16,6 @@ typedef struct GamepadAxisState { int16_t raw; - int16_t analog; int8_t digital; } GamepadAxisState; @@ -42,6 +41,9 @@ static struct { #define DEVNUM(dev) dynarray_indexof(&gamepad.devices, (dev)) +#define MIN_DEADZONE (4.0 / (GAMEPAD_AXIS_MAX_VALUE)) +#define MAX_DEADZONE (1 - MIN_DEADZONE) + static bool gamepad_event_handler(SDL_Event *event, void *arg); static int gamepad_load_mappings(const char *vpath, int warn_noexist) { @@ -338,16 +340,6 @@ bool gamepad_initialized(void) { return gamepad.initialized; } -static float gamepad_axis_sens(GamepadAxis id) { - if(id == config_get_int(CONFIG_GAMEPAD_AXIS_LR)) - return config_get_float(CONFIG_GAMEPAD_AXIS_LR_SENS); - - if(id == config_get_int(CONFIG_GAMEPAD_AXIS_UD)) - return config_get_float(CONFIG_GAMEPAD_AXIS_UD_SENS); - - return 1.0; -} - static int gamepad_axis2gameevt(GamepadAxis id) { if(id == config_get_int(CONFIG_GAMEPAD_AXIS_LR)) { return TE_GAME_AXIS_LR; @@ -360,55 +352,15 @@ static int gamepad_axis2gameevt(GamepadAxis id) { return -1; } -static int get_axis_abs_limit(int val) { - if(val < 0) { - return -GAMEPAD_AXIS_MIN_VALUE; - } - - return GAMEPAD_AXIS_MAX_VALUE; -} - -static int gamepad_axis_process_value_deadzone(int raw) { - int val, vsign; - int limit = get_axis_abs_limit(raw); - float deadzone = clamp(config_get_float(CONFIG_GAMEPAD_AXIS_DEADZONE), 0.01, 0.999); - int minval = clamp(deadzone, 0, 1) * limit; - - val = raw; - vsign = sign(val); - val = abs(val); - - if(val < minval) { - val = 0; - } else { - val = vsign * clamp((val - minval) / (1.0 - deadzone), 0, limit); - } - - return val; -} - -static int gamepad_axis_process_value(GamepadAxis id, int raw) { - double sens = gamepad_axis_sens(id); - int sens_sign = sign(sens); - - raw = gamepad_axis_process_value_deadzone(raw); - - int limit = get_axis_abs_limit(sens_sign * raw); - double x = raw / (double)limit; - int in_sign = sign(x); - - x = pow(fabs(x), 1.0 / fabs(sens)) * in_sign * sens_sign; - x = x ? x : 0; - x = clamp(x * limit, GAMEPAD_AXIS_MIN_VALUE, GAMEPAD_AXIS_MAX_VALUE); - - return (int)x; +static inline double gamepad_get_deadzone(void) { + return clamp(config_get_float(CONFIG_GAMEPAD_AXIS_DEADZONE), MIN_DEADZONE, MAX_DEADZONE); } int gamepad_axis_value(GamepadAxis id) { assert(id > GAMEPAD_AXIS_INVALID); assert(id < GAMEPAD_AXIS_MAX); - return gamepad.axes[id].analog; + return gamepad.axes[id].raw; } int gamepad_player_axis_value(GamepadPlrAxis paxis) { @@ -434,7 +386,7 @@ static void gamepad_axis(GamepadAxis id, int raw); double gamepad_normalize_axis_value(int val) { if(val < 0) { - return -val / (double)GAMEPAD_AXIS_MIN_VALUE; + return val / (double)-GAMEPAD_AXIS_MIN_VALUE; } else if(val > 0) { return val / (double)GAMEPAD_AXIS_MAX_VALUE; } else { @@ -444,25 +396,25 @@ double gamepad_normalize_axis_value(int val) { int gamepad_denormalize_axis_value(double val) { if(val < 0) { - return -val * GAMEPAD_AXIS_MIN_VALUE; + return imax(GAMEPAD_AXIS_MIN_VALUE, val * -GAMEPAD_AXIS_MIN_VALUE); } else if(val > 0) { - return val * GAMEPAD_AXIS_MAX_VALUE; + return imin(GAMEPAD_AXIS_MAX_VALUE, val * GAMEPAD_AXIS_MAX_VALUE); } else { return 0; } } static void gamepad_update_game_axis(GamepadAxis axis, int oldval) { - if(oldval != gamepad.axes[axis].analog) { + if(oldval != gamepad.axes[axis].raw) { int evt = gamepad_axis2gameevt(axis); if(evt >= 0) { - events_emit(evt, gamepad.axes[axis].analog, NULL, NULL); + events_emit(evt, gamepad.axes[axis].raw, NULL, NULL); } } } -static void gamepad_restrict_player_axis_vals(GamepadAxis new_axis, int new_val) { +static cmplx gamepad_restrict_analog_input(cmplx z) { typedef enum { UP = (1 << 0), DOWN = (1 << 1), @@ -470,19 +422,8 @@ static void gamepad_restrict_player_axis_vals(GamepadAxis new_axis, int new_val) LEFT = (1 << 2), } MoveDir; - GamepadAxis axis_x = config_get_int(CONFIG_GAMEPAD_AXIS_LR); - GamepadAxis axis_y = config_get_int(CONFIG_GAMEPAD_AXIS_UD); - - int old_x = gamepad.axes[axis_x].analog; - int old_y = gamepad.axes[axis_y].analog; - - gamepad.axes[new_axis].analog = new_val; - - assert(axis_x > GAMEPAD_AXIS_INVALID && axis_x < GAMEPAD_AXIS_MAX); - assert(axis_y > GAMEPAD_AXIS_INVALID && axis_y < GAMEPAD_AXIS_MAX); - - double x = gamepad_normalize_axis_value(gamepad_player_axis_value(PLRAXIS_LR)); - double y = gamepad_normalize_axis_value(gamepad_player_axis_value(PLRAXIS_UD)); + double x = creal(z); + double y = cimag(z); MoveDir move = 0; @@ -501,19 +442,93 @@ static void gamepad_restrict_player_axis_vals(GamepadAxis new_axis, int new_val) } } - gamepad.axes[axis_x].analog = (move & LEFT) ? GAMEPAD_AXIS_MIN_VALUE : - (move & RIGHT) ? GAMEPAD_AXIS_MAX_VALUE : 0; + x = (bool)(move & RIGHT) - (bool)(move & LEFT); + y = (bool)(move & UP) - (bool)(move & DOWN); - gamepad.axes[axis_y].analog = (move & DOWN) ? GAMEPAD_AXIS_MIN_VALUE : - (move & UP) ? GAMEPAD_AXIS_MAX_VALUE : 0; + return CMPLX(x, y); +} - gamepad_update_game_axis(axis_x, old_x); - gamepad_update_game_axis(axis_y, old_y); +static double gamepad_apply_sensitivity(double p, double sens) { + if(sens == 0) { + return p; + } + + double t = exp2(sens); + double a = 1 - pow(1 - fabs(p), t); + + if(isnan(a)) { + return p; + } + + return copysign(pow(a, 1 / t), p); +} + +static cmplx square_to_circle(cmplx z) { + double u = creal(z) * sqrt(1.0 - cimag(z) * cimag(z) / 2.0); + double v = cimag(z) * sqrt(1.0 - creal(z) * creal(z) / 2.0); + + return CMPLX(u, v); +} + +void gamepad_get_player_analog_input(int *xaxis, int *yaxis) { + int xraw = gamepad_player_axis_value(PLRAXIS_LR); + int yraw = gamepad_player_axis_value(PLRAXIS_UD); + + *xaxis = 0; + *yaxis = 0; + + double deadzone = gamepad_get_deadzone(); + double maxzone = config_get_float(CONFIG_GAMEPAD_AXIS_MAXZONE); + + cmplx z = CMPLX( + gamepad_normalize_axis_value(xraw), + gamepad_normalize_axis_value(yraw) + ); + + if(config_get_int(CONFIG_GAMEPAD_AXIS_SQUARECIRCLE)) { + z = square_to_circle(z); + } + + double raw_abs2 = cabs2(z); + + if(raw_abs2 < deadzone * deadzone) { + return; + } + + double raw_abs = sqrt(raw_abs2); + assert(raw_abs > 0); + + double new_abs = (raw_abs - deadzone) / (maxzone - deadzone); + new_abs = gamepad_apply_sensitivity(new_abs, config_get_float(CONFIG_GAMEPAD_AXIS_SENS)); + z *= new_abs / raw_abs; + + z = CMPLX( + gamepad_apply_sensitivity(creal(z), config_get_float(CONFIG_GAMEPAD_AXIS_LR_SENS)), + gamepad_apply_sensitivity(cimag(z), config_get_float(CONFIG_GAMEPAD_AXIS_UD_SENS)) + ); + + if(!config_get_int(CONFIG_GAMEPAD_AXIS_FREE)) { + z = gamepad_restrict_analog_input(z); + } + + *xaxis = gamepad_denormalize_axis_value(creal(z)); + *yaxis = gamepad_denormalize_axis_value(cimag(z)); +} + +static int gamepad_axis_get_digital_value(int raw) { + double deadzone = gamepad_get_deadzone(); + deadzone = fmax(deadzone, 0.5); + int threshold = GAMEPAD_AXIS_MAX_VALUE * deadzone; + + if(abs(raw) < threshold) { + return 0; + } + + return raw < 0 ? -1 : 1; } static void gamepad_axis(GamepadAxis id, int raw) { - int8_t digital = AXISVAL(gamepad_axis_process_value_deadzone(raw)); - int16_t analog = gamepad_axis_process_value(id, raw); + int8_t digital = gamepad_axis_get_digital_value(raw); if(digital * gamepad.axes[id].digital < 0) { // axis changed direction without passing the '0' state @@ -524,19 +539,14 @@ static void gamepad_axis(GamepadAxis id, int raw) { events_emit(TE_GAMEPAD_AXIS, id, (void*)(intptr_t)raw, NULL); - int old_analog = gamepad.axes[id].analog; + int old_raw = gamepad.axes[id].raw; gamepad.axes[id].raw = raw; - - if(config_get_int(CONFIG_GAMEPAD_AXIS_FREE)) { - gamepad.axes[id].analog = analog; - gamepad_update_game_axis(id, old_analog); - } else if(gamepad_axis2gameevt(id) >= 0) { - gamepad_restrict_player_axis_vals(id, analog); - } + gamepad_update_game_axis(id, old_raw); if(digital != AXISVAL_NULL) { // simulate press if(!gamepad.axes[id].digital) { gamepad.axes[id].digital = digital; + events_emit(TE_GAMEPAD_AXIS_DIGITAL, id, (void*)(intptr_t)digital, NULL); GamepadButton btn = gamepad_button_from_axis(id, digital); @@ -547,6 +557,7 @@ static void gamepad_axis(GamepadAxis id, int raw) { } else if(gamepad.axes[id].digital != AXISVAL_NULL) { // simulate release GamepadButton btn = gamepad_button_from_axis(id, gamepad.axes[id].digital); gamepad.axes[id].digital = AXISVAL_NULL; + events_emit(TE_GAMEPAD_AXIS_DIGITAL, id, (void*)(intptr_t)digital, NULL); if(btn != GAMEPAD_BUTTON_INVALID) { gamepad_button(btn, SDL_RELEASED, false); diff --git a/src/gamepad.h b/src/gamepad.h index c518e804..66d43191 100644 --- a/src/gamepad.h +++ b/src/gamepad.h @@ -131,10 +131,10 @@ SDL_GameControllerAxis gamepad_axis_to_sdl_axis(GamepadAxis axis); int gamepad_axis_value(GamepadAxis paxis); int gamepad_player_axis_value(GamepadPlrAxis paxis); +void gamepad_get_player_analog_input(int *xaxis, int *yaxis); double gamepad_normalize_axis_value(int val); int gamepad_denormalize_axis_value(double val); #define GAMEPAD_AXIS_MAX_VALUE 32767 #define GAMEPAD_AXIS_MIN_VALUE -32768 -#define AXISVAL sign diff --git a/src/menu/options.c b/src/menu/options.c index ac7b68d5..127aa519 100644 --- a/src/menu/options.c +++ b/src/menu/options.c @@ -119,6 +119,7 @@ static OptionBinding* bind_gpbinding(int cfgentry) { } // BT_GamepadAxisBinding: gamepad axis mapping options +attr_unused static OptionBinding* bind_gpaxisbinding(int cfgentry) { OptionBinding *bind = bind_new(); @@ -854,31 +855,29 @@ static MenuData* create_options_menu_gamepad(MenuData *parent) { add_menu_separator(m); - add_menu_entry(m, "X axis", do_nothing, - b = bind_gpaxisbinding(CONFIG_GAMEPAD_AXIS_LR) - ); - - add_menu_entry(m, "Y axis", do_nothing, - b = bind_gpaxisbinding(CONFIG_GAMEPAD_AXIS_UD) - ); - add_menu_entry(m, "Axes mode", do_nothing, b = bind_option(CONFIG_GAMEPAD_AXIS_FREE, bind_common_onoff_get, bind_common_onoff_set) ); bind_addvalue(b, "free"); bind_addvalue(b, "restricted"); - add_menu_entry(m, "X axis sensitivity", do_nothing, - b = bind_scale(CONFIG_GAMEPAD_AXIS_LR_SENS, -2, 2, 0.05) - ); b->pad++; + add_menu_entry(m, "Remap square input into circular", do_nothing, + b = bind_option(CONFIG_GAMEPAD_AXIS_SQUARECIRCLE, bind_common_onoff_get, bind_gamepad_set) + ); bind_onoff(b); - add_menu_entry(m, "Y axis sensitivity", do_nothing, - b = bind_scale(CONFIG_GAMEPAD_AXIS_UD_SENS, -2, 2, 0.05) - ); b->pad++; + add_menu_separator(m); add_menu_entry(m, "Dead zone", do_nothing, b = bind_scale(CONFIG_GAMEPAD_AXIS_DEADZONE, 0, 1, 0.01) ); + add_menu_entry(m, "Maximum zone", do_nothing, + b = bind_scale(CONFIG_GAMEPAD_AXIS_MAXZONE, 0, 1, 0.01) + ); + + add_menu_entry(m, "Sensitivity", do_nothing, + b = bind_scale(CONFIG_GAMEPAD_AXIS_SENS, -2, 2, 0.05) + ); + add_menu_separator(m); add_menu_entry(m, "Back", menu_action_close, NULL); @@ -1533,7 +1532,7 @@ static bool options_rebind_input_handler(SDL_Event *event, void *arg) { return true; } - if(t == MAKE_TAISEI_EVENT(TE_GAMEPAD_AXIS)) { + if(t == MAKE_TAISEI_EVENT(TE_GAMEPAD_AXIS_DIGITAL)) { GamepadAxis axis = event->user.code; if(b->type == BT_GamepadAxisBinding) { diff --git a/src/player.c b/src/player.c index 6a10e2e1..91eca61c 100644 --- a/src/player.c +++ b/src/player.c @@ -1360,8 +1360,8 @@ void player_fix_input(Player *plr, ReplayState *rpy_out) { player_event(plr, NULL, rpy_out, EV_INFLAGS, newflags); } - int axis_lr = gamepad_player_axis_value(PLRAXIS_LR); - int axis_ud = gamepad_player_axis_value(PLRAXIS_UD); + int axis_lr, axis_ud; + gamepad_get_player_analog_input(&axis_lr, &axis_ud); if(plr->axis_lr != axis_lr) { player_event(plr, NULL, rpy_out, EV_AXIS_LR, axis_lr); diff --git a/src/replay/demoplayer.c b/src/replay/demoplayer.c index 04584118..72ba6a32 100644 --- a/src/replay/demoplayer.c +++ b/src/replay/demoplayer.c @@ -145,7 +145,7 @@ static bool demoplayer_activity_event(SDL_Event *evt, void *arg) { default: switch(TAISEI_EVENT(evt->type)) { case TE_GAMEPAD_BUTTON_DOWN: - case TE_GAMEPAD_AXIS: + case TE_GAMEPAD_AXIS_DIGITAL: goto reset; } diff --git a/src/stage.c b/src/stage.c index 133e6d9f..43e6bb6e 100644 --- a/src/stage.c +++ b/src/stage.c @@ -457,14 +457,6 @@ static bool stage_input_handler_gameplay(SDL_Event *event, void *arg) { } break; - case TE_GAME_AXIS_LR: - player_event(&global.plr, NULL, rpy, EV_AXIS_LR, (uint16_t)code); - break; - - case TE_GAME_AXIS_UD: - player_event(&global.plr, NULL, rpy, EV_AXIS_UD, (uint16_t)code); - break; - default: break; } @@ -484,6 +476,7 @@ static bool stage_input_handler_demo(SDL_Event *event, void *arg) { switch(TAISEI_EVENT(event->type)) { case TE_GAME_KEY_DOWN: case TE_GAMEPAD_BUTTON_DOWN: + case TE_GAMEPAD_AXIS_DIGITAL: exit: stage_finish(GAMEOVER_ABORT); }