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
This commit is contained in:
Andrei Alexeyev 2023-07-11 23:51:44 +02:00
parent 115948857e
commit b57445900f
No known key found for this signature in database
GPG key ID: 72D26128040B9690
9 changed files with 136 additions and 120 deletions

View file

@ -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) {

View file

@ -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 \

View file

@ -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,

View file

@ -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);

View file

@ -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

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}

View file

@ -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);
}