"Flick Stick" controller layout implementation

With southpaw version added to "sticks layout" menu
Cvar for length of stick to be considered a flick or rotation
Lacks rotation smoothing
This commit is contained in:
Jaime Moreira 2022-08-24 19:02:21 -04:00
parent 0417bc1023
commit 748909fd96
3 changed files with 135 additions and 8 deletions

View file

@ -476,6 +476,9 @@ Set `0` by default.
- `2`: *Legacy*, left moves forward/backward and turns, right strafes
and looks up/down
- `3`: *Legacy Southpaw*, inverted sticks version of previous one
- `4`: *Flick Stick*, left stick moves, right checks your surroundings
in 360º, gyro required for looking up/down
- `5`: *Flick Stick Southpaw*, swapped sticks version of last one
* **joy_left_deadzone** / **joy_right_deadzone**: Inner, circular
deadzone for each stick, where inputs below this radius will be
@ -486,13 +489,18 @@ Set `0` by default.
deadzone with the shape of a "bowtie", which will help you to do
perfectly horizontal or vertical movements the more you mark a
direction with the stick. Increasing this too much will reduce speed
for the diagonals. Default `0.15`.
for the diagonals, but will help you to mark 90º/180º turns with Flick
Stick. Default `0.15`.
* **joy_left_expo** / **joy_right_expo**: Exponents on the response
curve on each stick. Increasing this will make small movements to
represent much smaller inputs, which helps precision with the sticks.
`1.0` is linear. Default `2.0` (quadratic curve).
* **joy_flick_threshold**: Used only with Flick Stick, specifies the
distance from the center of the stick that will make the player flick
or rotate. Default `0.65` (65%).
* **gyro_mode**: Operation mode for the gyroscope sensor of the game
controller. Options are `0` = always off, `1` = off with the
`+gyroaction` bind to enable, `2` = on with `+gyroaction` to

View file

@ -24,6 +24,9 @@
* - http://quakespasm.sourceforge.net
* - https://github.com/Minimuino/thumbstick-deadzones
*
* Flick Stick handling is based on:
* http://gyrowiki.jibbsmart.com/blog:good-gyro-controls-part-2:the-flick-stick
*
* =======================================================================
*
* This is the Quake II input system backend, implemented with SDL.
@ -51,7 +54,9 @@ enum {
LAYOUT_DEFAULT = 0,
LAYOUT_SOUTHPAW,
LAYOUT_LEGACY,
LAYOUT_LEGACY_SOUTHPAW
LAYOUT_LEGACY_SOUTHPAW,
LAYOUT_FLICK_STICK,
LAYOUT_FLICK_STICK_SOUTHPAW
};
typedef struct
@ -142,6 +147,7 @@ static cvar_t *joy_left_deadzone;
static cvar_t *joy_right_expo;
static cvar_t *joy_right_snapaxis;
static cvar_t *joy_right_deadzone;
static cvar_t *joy_flick_threshold;
// Joystick haptic
static cvar_t *joy_haptic_magnitude;
@ -180,6 +186,11 @@ static updates_countdown_reasons countdown_reason = REASON_CONTROLLERINIT;
// Factor used to transform from SDL input to Q2 "view angle" change
static float normalize_sdl_gyro = 1.0f / M_PI; // can change depending on hardware
// Flick Stick
#define FLICK_TIME 6 // number of frames it takes for a flick to execute
static float target_angle; // angle to end up facing at the end of a flick
static unsigned short int flick_progress = FLICK_TIME;
extern void CalibrationFinishedCallback(void);
/* ------------------------------------------------------------------ */
@ -952,6 +963,59 @@ IN_ApplyExpo(thumbstick_t stick, float exponent)
return result;
}
/*
* Flick Stick handling: detect if the player just started one, or return the
* player rotation if stick was already flicked
*/
static float
IN_FlickStick(thumbstick_t stick, float axial_deadzone)
{
static qboolean is_flicking;
static float last_stick_angle;
thumbstick_t processed = stick;
float angle_change = 0;
if (IN_StickMagnitude(stick) > min(joy_flick_threshold->value, 1.0f)) // flick!
{
// Make snap-to-axis only if player wasn't already flicking
if (!is_flicking || flick_progress < FLICK_TIME)
{
processed = IN_SlopedAxialDeadzone(stick, axial_deadzone);
}
const float stick_angle = (180 / M_PI) * atan2f(-processed.x, -processed.y);
if (!is_flicking)
{
// Flicking begins now, with a new target
is_flicking = true;
flick_progress = 0;
target_angle = stick_angle;
}
else
{
// Was already flicking, just turning now
angle_change = stick_angle - last_stick_angle;
// angle wrap: https://stackoverflow.com/a/11498248/1130520
angle_change = fmod(angle_change + 180.0f, 360.0f);
if (angle_change < 0)
{
angle_change += 360.0f;
}
angle_change -= 180.0f;
}
last_stick_angle = stick_angle;
}
else
{
is_flicking = false;
}
return angle_change;
}
/*
* Move handling
*/
@ -961,6 +1025,12 @@ IN_Move(usercmd_t *cmd)
// Factor used to transform from SDL joystick input ([-32768, 32767]) to [-1, 1] range
static const float normalize_sdl_axis = 1.0f / 32768.0f;
// Flick Stick's factors to change to the target angle with a feeling of "ease out"
static const float rotation_factor[FLICK_TIME] =
{
0.305555556f, 0.249999999f, 0.194444445f, 0.138888889f, 0.083333333f, 0.027777778f
};
static float old_mouse_x;
static float old_mouse_y;
static float joystick_yaw, joystick_pitch;
@ -1049,16 +1119,30 @@ IN_Move(usercmd_t *cmd)
if (left_stick.x || left_stick.y)
{
left_stick = IN_RadialDeadzone(left_stick, joy_left_deadzone->value);
if ((int)joy_layout->value == LAYOUT_FLICK_STICK_SOUTHPAW)
{
cl.viewangles[YAW] += IN_FlickStick(left_stick, joy_left_snapaxis->value);
}
else
{
left_stick = IN_SlopedAxialDeadzone(left_stick, joy_left_snapaxis->value);
left_stick = IN_ApplyExpo(left_stick, joy_left_expo->value);
}
}
if (right_stick.x || right_stick.y)
{
right_stick = IN_RadialDeadzone(right_stick, joy_right_deadzone->value);
if ((int)joy_layout->value == LAYOUT_FLICK_STICK)
{
cl.viewangles[YAW] += IN_FlickStick(right_stick, joy_right_snapaxis->value);
}
else
{
right_stick = IN_SlopedAxialDeadzone(right_stick, joy_right_snapaxis->value);
right_stick = IN_ApplyExpo(right_stick, joy_right_expo->value);
}
}
switch((int)joy_layout->value)
{
@ -1080,6 +1164,14 @@ IN_Move(usercmd_t *cmd)
joystick_yaw = right_stick.x;
joystick_pitch = left_stick.y;
break;
case LAYOUT_FLICK_STICK: // yaw already set by now
joystick_forwardmove = left_stick.y;
joystick_sidemove = left_stick.x;
break;
case LAYOUT_FLICK_STICK_SOUTHPAW:
joystick_forwardmove = right_stick.y;
joystick_sidemove = right_stick.x;
break;
default: // LAYOUT_DEFAULT
joystick_forwardmove = left_stick.y;
joystick_sidemove = left_stick.x;
@ -1129,6 +1221,13 @@ IN_Move(usercmd_t *cmd)
{
cl.viewangles[PITCH] -= (m_pitch->value * gyro_pitch) * gyroViewFactor;
}
// Flick Stick: flick in progress, changing the yaw angle to the target progressively
if (flick_progress < FLICK_TIME)
{
cl.viewangles[YAW] += target_angle * rotation_factor[flick_progress];
flick_progress++;
}
}
/* ------------------------------------------------------------------ */
@ -1664,6 +1763,7 @@ IN_Init(void)
joy_right_expo = Cvar_Get("joy_right_expo", "2.0", CVAR_ARCHIVE);
joy_right_snapaxis = Cvar_Get("joy_right_snapaxis", "0.15", CVAR_ARCHIVE);
joy_right_deadzone = Cvar_Get("joy_right_deadzone", "0.16", CVAR_ARCHIVE);
joy_flick_threshold = Cvar_Get("joy_flick_threshold", "0.65", CVAR_ARCHIVE);
gyro_calibration_x = Cvar_Get("gyro_calibration_x", "0.0", CVAR_ARCHIVE);
gyro_calibration_y = Cvar_Get("gyro_calibration_y", "0.0", CVAR_ARCHIVE);

View file

@ -1814,6 +1814,17 @@ Joy_MenuInit(void)
0
};
static const char *stick_layouts_fs[] =
{
"default",
"southpaw",
"legacy",
"legacy southpaw",
"flick stick",
"flick stick spaw",
0
};
int y = 0;
s_joy_menu.x = (int)(viddef.width * 0.50f);
@ -1905,8 +1916,16 @@ Joy_MenuInit(void)
y += 10;
s_joy_layout_box.generic.name = "stick layout";
s_joy_layout_box.generic.callback = StickLayoutFunc;
if (gyro_hardware || joy_layout->value > 3)
{
s_joy_layout_box.itemnames = stick_layouts_fs;
s_joy_layout_box.curvalue = ClampCvar(0, 5, joy_layout->value);
}
else
{
s_joy_layout_box.itemnames = stick_layouts;
s_joy_layout_box.curvalue = ClampCvar(0, 3, joy_layout->value);
}
Menu_AddItem(&s_joy_menu, (void *)&s_joy_layout_box);
if (gyro_hardware)