Skip to content

Commit

Permalink
gamepad: direction snapping; more flexible configuration options
Browse files Browse the repository at this point in the history
- The free/restricted axis distinction is gone; the joystick always
  operates in "free" mode.
- Added direction snapping functionality to help aid exact movement in
  cardinal and/or diagonal directions. The snapping angle can be
  adjusted from 0% (disabled) to 100% (similar to the old "restricted"
  mode). The snapping angle can also be biased towards cardinals or
  diagonals.
- When the maximum zone is less than or equals dead zone, moving
  the character will always move at maximum speed (as in the old
  "restricted" mode).
- Most of these settings are now visualized in the options menu and can
  be tested there.
  • Loading branch information
Akaricchi committed May 27, 2024
1 parent d288f2b commit 7548e48
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 72 deletions.
90 changes: 90 additions & 0 deletions resources/00-taisei.pkgdir/shader/gamepad_circle.frag.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#version 330 core

#include "lib/render_context.glslh"
#include "lib/util.glslh"
#include "interface/standard.glslh"

UNIFORM(1) vec4 snap_angles_sincos;
UNIFORM(2) vec4 joy_pointers;
UNIFORM(3) vec2 deadzones;

float sdCircle(vec2 p, float r) {
return length(p) - r;
}

float sdRoundedX(vec2 p, float w, float r) {
p = abs(p);
return length(p - min(p.x + p.y, w) * 0.5) - r;
}

float sdPieDual(vec2 p, vec2 c, float r) {
p = abs(p);
float l = length(p) - r;
float m = length(p - c * clamp(dot(p, c), 0.0, r));
return max(l, m * sign(c.y * p.x - c.x * p.y));
}

vec2 sincos(float a) {
return vec2(sin(a), cos(a));
}

float zones(vec2 uv, vec2 sc) {
return 0.5 - min(
sdPieDual(uv, sc, 1.0),
sdPieDual(uv.yx, sc, 1.0));
}

vec4 joystickPointer(vec2 p, float d, vec4 color) {
float base = 1.0 - sdCircle(p, -0.4);

float outer = smoothstep(0.48, 0.52, base) - smoothstep(0.513-d, 0.513+d, base);
vec4 c = mix(vec4(color.a), color, outer) * outer;

float inner = smoothstep(0.51, 0.53, base);
float innerGrad = smoothstep(0.52, 0.55, base);
c = alphaCompose(c, vec4(color.rgb * mix(1.0, 0.75, innerGrad), color.a) * inner);

return c * c.a;
}

void main(void) {
vec2 uv = 2.22 * (flip_native_to_topleft(texCoordRaw) - 0.5);
vec2 snapCardinalsSinCos = snap_angles_sincos.xy;
vec2 snapDiagonalsSinCos = snap_angles_sincos.zw;

vec4 c = vec4(0);

float d = fwidth(uv.x);
float circleBase = 1.0 - sdCircle(uv, 0.5);
float circle = smoothstep(0.5 - d, 0.5 + d, circleBase);
c = alphaCompose(c, vec4(0.25 * circle));

float deadzone = smoothstep(0.5 - 0.01, 0.5 + 0.01, circleBase - (1 - deadzones.x));
float snapMask = circle * (1 - deadzone);
deadzone *= 1.0 - smoothstep(0.25, 0.7, circleBase - (1 - deadzones.x + 0.2));

float maxzone_val = 1 - max(deadzones.x, deadzones.y);
float maxzone = smoothstep(0.5 - maxzone_val, 0.5 + d, circleBase - maxzone_val);
maxzone -= smoothstep(0.52 - d, 0.52 + d, circleBase - maxzone_val);

float cross1 = sdRoundedX(uv * rot(pi/4), 2.0, -0.49);
float cross2 = sdRoundedX(uv, 2.0, -0.49);
float delimiters = smoothstep(0.48, 0.6, circle * (1.0 - min(cross1, cross2)));

float cardinals = zones(uv, snapCardinalsSinCos);
float diagonals = zones(uv * rot(pi/4), snapDiagonalsSinCos);

c = alphaCompose(c, vec4(0.5 * maxzone));
c = alphaCompose(c, vec4(1, 0.3, 0.3, 1) * 0.5 * snapMask * smoothstep(0.5-d, 0.5+d, cardinals));
c = alphaCompose(c, vec4(0.3, 0.3, 1, 1) * 0.5 * snapMask * smoothstep(0.5-d, 0.5+d, diagonals));
c = alphaCompose(c, vec4(vec3(0), 0.7 * deadzone));
c = alphaCompose(c, joystickPointer(uv - joy_pointers.xy, d, vec4(1, 0.9, 0.2, 1)));
c = alphaCompose(c, joystickPointer(uv - joy_pointers.zw, d, vec4(0.6)));
c = alphaCompose(c, vec4(0, 0, 0, delimiters));

if(all(lessThan(c, vec4(1.0 / 255.0)))) {
discard;
}

fragColor = c * r_color;
}
2 changes: 2 additions & 0 deletions resources/00-taisei.pkgdir/shader/gamepad_circle.prog
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

objects = standard.vert gamepad_circle.frag
1 change: 1 addition & 0 deletions resources/00-taisei.pkgdir/shader/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ glsl_files = [
'fireparticles.vert.glsl',
'fxaa.frag.glsl',
'fxaa.vert.glsl',
'gamepad_circle.frag.glsl',
'glitch.frag.glsl',
'graph.frag.glsl',
'healthbar.vert.glsl',
Expand Down
3 changes: 2 additions & 1 deletion src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,14 @@
CONFIGDEF_STRING (GAMEPAD_DEVICE, "gamepad_device", "any") \
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_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_AXIS_MAXZONE, "gamepad_axis_maxzone", 0.9) \
CONFIGDEF_FLOAT (GAMEPAD_AXIS_SNAP, "gamepad_axis_snap", 0.5) \
CONFIGDEF_FLOAT (GAMEPAD_AXIS_SNAP_DIAG_BIAS, "gamepad_axis_snap_diagonal_bias", 0.0) \
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 \
Expand Down
107 changes: 63 additions & 44 deletions src/gamepad.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "hirestime.h"
#include "log.h"
#include "transition.h"
#include "util.h"
#include "util/miscmath.h"
#include "util/stringops.h"
#include "vfs/public.h"
Expand Down Expand Up @@ -357,10 +358,14 @@ static int gamepad_axis2gameevt(GamepadAxis id) {
return -1;
}

static inline double gamepad_get_deadzone(void) {
double gamepad_get_normalized_deadzone(void) {
return clamp(config_get_float(CONFIG_GAMEPAD_AXIS_DEADZONE), MIN_DEADZONE, MAX_DEADZONE);
}

double gamepad_get_normalized_maxzone(void) {
return clamp(config_get_float(CONFIG_GAMEPAD_AXIS_MAXZONE), 0, 1);
}

int gamepad_axis_value(GamepadAxis id) {
assert(id > GAMEPAD_AXIS_INVALID);
assert(id < GAMEPAD_AXIS_MAX);
Expand Down Expand Up @@ -419,40 +424,6 @@ static void gamepad_update_game_axis(GamepadAxis axis, int oldval) {
}
}

static cmplx gamepad_restrict_analog_input(cmplx z) {
typedef enum {
UP = (1 << 0),
DOWN = (1 << 1),
RIGHT = (1 << 3),
LEFT = (1 << 2),
} MoveDir;

double x = re(z);
double y = im(z);

MoveDir move = 0;

if(x || y) {
int d = (int)rint(atan2(-y, x) / (M_PI/4));

switch(d) {
case 0: move = 0 | RIGHT; break;
case -1: move = UP | RIGHT; break;
case -2: move = UP | 0; break;
case -3: move = UP | LEFT; break;
case -4: case 4: move = 0 | LEFT; break;
case 3: move = DOWN | LEFT; break;
case 2: move = DOWN | 0; break;
case 1: move = DOWN | RIGHT; break;
}
}

x = (bool)(move & RIGHT) - (bool)(move & LEFT);
y = (bool)(move & UP) - (bool)(move & DOWN);

return CMPLX(x, y);
}

static double gamepad_apply_sensitivity(double p, double sens) {
if(sens == 0) {
return p;
Expand All @@ -475,15 +446,56 @@ static cmplx square_to_circle(cmplx z) {
return CMPLX(u, v);
}

static cmplx gamepad_snap_analog_direction(cmplx z) {
double snap = clamp(config_get_float(CONFIG_GAMEPAD_AXIS_SNAP), 0, 1);

if(snap == 0) {
return z;
}

double diagonal_bias = clamp(config_get_float(CONFIG_GAMEPAD_AXIS_SNAP_DIAG_BIAS), -1, 1);
double w_cardinal = 1 - diagonal_bias;
double w_diagonal = 1 + diagonal_bias;

#define D 0.7071067811865475
struct { cmplx v; double weight; } directions[] = {
{ CMPLX( 1, 0), w_cardinal },
{ CMPLX( 0, 1), w_cardinal },
{ CMPLX(-1, 0), w_cardinal },
{ CMPLX( 0, -1), w_cardinal },
{ CMPLX( D, D), w_diagonal },
{ CMPLX( D, -D), w_diagonal },
{ CMPLX(-D, -D), w_diagonal },
{ CMPLX(-D, D), w_diagonal },
};
#undef D

double m = cabs(z);
double thres = snap * M_PI/ARRAY_SIZE(directions);

for(int i = 0; i < ARRAY_SIZE(directions); ++i) {
cmplx q = directions[i].v;
double delta_angle = acos(cdot(q, z) / m);

if(delta_angle < thres * directions[i].weight) {
z = q * m;
break;
}

}

return z;
}

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);
double deadzone = gamepad_get_normalized_deadzone();
double maxzone = gamepad_get_normalized_maxzone();

cmplx z = CMPLX(
gamepad_normalize_axis_value(xraw),
Expand All @@ -503,25 +515,32 @@ void gamepad_get_player_analog_input(int *xaxis, int *yaxis) {
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;
if(deadzone < maxzone) {
double new_abs;
if(raw_abs >= maxzone) {
new_abs = max(raw_abs, 1);
} else {
new_abs = (min(raw_abs, maxzone) - deadzone) / (maxzone - deadzone);
new_abs = gamepad_apply_sensitivity(new_abs, config_get_float(CONFIG_GAMEPAD_AXIS_SENS));
}
z *= new_abs / raw_abs;
} else {
z /= raw_abs;
}

z = CMPLX(
gamepad_apply_sensitivity(re(z), config_get_float(CONFIG_GAMEPAD_AXIS_LR_SENS)),
gamepad_apply_sensitivity(im(z), config_get_float(CONFIG_GAMEPAD_AXIS_UD_SENS))
);

if(!config_get_int(CONFIG_GAMEPAD_AXIS_FREE)) {
z = gamepad_restrict_analog_input(z);
}
z = gamepad_snap_analog_direction(z);

*xaxis = gamepad_denormalize_axis_value(re(z));
*yaxis = gamepad_denormalize_axis_value(im(z));
}

static int gamepad_axis_get_digital_value(int raw) {
double deadzone = gamepad_get_deadzone();
double deadzone = gamepad_get_normalized_deadzone();
deadzone = max(deadzone, 0.5);
int threshold = GAMEPAD_AXIS_MAX_VALUE * deadzone;

Expand Down
3 changes: 3 additions & 0 deletions src/gamepad.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,8 @@ void gamepad_get_player_analog_input(int *xaxis, int *yaxis);
double gamepad_normalize_axis_value(int val);
int gamepad_denormalize_axis_value(double val);

double gamepad_get_normalized_deadzone(void);
double gamepad_get_normalized_maxzone(void);

#define GAMEPAD_AXIS_MAX_VALUE 32767
#define GAMEPAD_AXIS_MIN_VALUE -32768
1 change: 1 addition & 0 deletions src/menu/mainmenu.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ void menu_preload(ResourceGroup *rg) {
NULL);

res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT,
"gamepad_circle",
"mainmenubg",
"sprite_circleclipped_indicator",
NULL);
Expand Down
Loading

0 comments on commit 7548e48

Please sign in to comment.