Controller Rumble / force feedback

Replaces classic "haptic" implementation, no longer available for
game controllers as of SDL 2.0.14.
This commit is contained in:
Jaime Moreira 2022-10-23 12:05:06 -03:00
parent 5c560039d8
commit 5a22720735
4 changed files with 155 additions and 313 deletions

View file

@ -285,10 +285,8 @@ void Key_ReadConsoleHistory();
void Key_WriteConsoleHistory();
void Key_SetBinding(int keynum, char *binding);
void Key_MarkAllUp(void);
void Haptic_Feedback(char *name, int effect_volume, int effect_duration,
int effect_begin, int effect_end,
int effect_attack, int effect_fade,
int effect_x, int effect_y, int effect_z);
void Controller_Rumble(const char *name, vec3_t source, qboolean from_player,
unsigned int duration, unsigned short int volume);
int Key_GetMenuKey(int key);
#endif

View file

@ -108,31 +108,10 @@ static cvar_t *windowed_mouse;
// ----
struct hapric_effects_cache {
int effect_volume;
int effect_duration;
int effect_begin;
int effect_end;
int effect_attack;
int effect_fade;
int effect_id;
int effect_x;
int effect_y;
int effect_z;
};
qboolean show_haptic = false;
qboolean show_haptic;
static SDL_Haptic *joystick_haptic = NULL;
static SDL_GameController *controller = NULL;
#define HAPTIC_EFFECT_LIST_SIZE 16
static int last_haptic_volume = 0;
static int last_haptic_efffect_size = HAPTIC_EFFECT_LIST_SIZE;
static int last_haptic_efffect_pos = 0;
static struct hapric_effects_cache last_haptic_efffect[HAPTIC_EFFECT_LIST_SIZE];
// Joystick sensitivity
static cvar_t *joy_yawsensitivity;
static cvar_t *joy_pitchsensitivity;
@ -1386,232 +1365,138 @@ In_FlushQueue(void)
/* ------------------------------------------------------------------ */
static void IN_Haptic_Shutdown(void);
/*
* Init haptic effects
*/
static int
IN_Haptic_Effect_Init(int effect_x, int effect_y, int effect_z,
int period, int magnitude,
int delay, int attack, int fade)
{
static SDL_HapticEffect haptic_effect;
/* limit magnitude */
if (magnitude > SHRT_MAX)
{
magnitude = SHRT_MAX;
}
else if (magnitude < 0)
{
magnitude = 0;
}
SDL_memset(&haptic_effect, 0, sizeof(SDL_HapticEffect)); // 0 is safe default
haptic_effect.type = SDL_HAPTIC_SINE;
haptic_effect.periodic.direction.type = SDL_HAPTIC_CARTESIAN; // Cartesian/3d coordinates
haptic_effect.periodic.direction.dir[0] = effect_x;
haptic_effect.periodic.direction.dir[1] = effect_y;
haptic_effect.periodic.direction.dir[2] = effect_z;
haptic_effect.periodic.period = period;
haptic_effect.periodic.magnitude = magnitude;
haptic_effect.periodic.length = period;
haptic_effect.periodic.delay = delay;
haptic_effect.periodic.attack_length = attack;
haptic_effect.periodic.fade_length = fade;
int effect_id = SDL_HapticNewEffect(joystick_haptic, &haptic_effect);
if (effect_id < 0)
{
Com_Printf ("SDL_HapticNewEffect failed: %s\n", SDL_GetError());
Com_Printf ("Please try to rerun game. Effects will be disabled for now.\n");
IN_Haptic_Shutdown();
}
return effect_id;
}
static void
IN_Haptic_Effects_Info(void)
{
show_haptic = true;
Com_Printf ("Joystick/Mouse haptic:\n");
Com_Printf (" * %d effects\n", SDL_HapticNumEffects(joystick_haptic));
Com_Printf (" * %d effects in same time\n", SDL_HapticNumEffectsPlaying(joystick_haptic));
Com_Printf (" * %d haptic axis\n", SDL_HapticNumAxes(joystick_haptic));
}
static void
IN_Haptic_Effects_Init(void)
{
last_haptic_efffect_size = SDL_HapticNumEffectsPlaying(joystick_haptic);
if (last_haptic_efffect_size > HAPTIC_EFFECT_LIST_SIZE)
{
last_haptic_efffect_size = HAPTIC_EFFECT_LIST_SIZE;
}
for (int i=0; i<HAPTIC_EFFECT_LIST_SIZE; i++)
{
last_haptic_efffect[i].effect_id = -1;
last_haptic_efffect[i].effect_volume = 0;
last_haptic_efffect[i].effect_duration = 0;
last_haptic_efffect[i].effect_begin = 0;
last_haptic_efffect[i].effect_end = 0;
last_haptic_efffect[i].effect_attack = 0;
last_haptic_efffect[i].effect_fade = 0;
last_haptic_efffect[i].effect_x = 0;
last_haptic_efffect[i].effect_y = 0;
last_haptic_efffect[i].effect_z = 0;
}
}
/*
* Shuts the backend down
*/
static void
IN_Haptic_Effect_Shutdown(int * effect_id)
{
if (!effect_id)
{
return;
}
if (*effect_id >= 0)
{
SDL_HapticDestroyEffect(joystick_haptic, *effect_id);
}
*effect_id = -1;
}
static void
IN_Haptic_Effects_Shutdown(void)
{
for (int i=0; i<HAPTIC_EFFECT_LIST_SIZE; i++)
{
last_haptic_efffect[i].effect_volume = 0;
last_haptic_efffect[i].effect_duration = 0;
last_haptic_efffect[i].effect_begin = 0;
last_haptic_efffect[i].effect_end = 0;
last_haptic_efffect[i].effect_attack = 0;
last_haptic_efffect[i].effect_fade = 0;
last_haptic_efffect[i].effect_x = 0;
last_haptic_efffect[i].effect_y = 0;
last_haptic_efffect[i].effect_z = 0;
IN_Haptic_Effect_Shutdown(&last_haptic_efffect[i].effect_id);
}
}
/*
* Haptic Feedback:
* effect_volume=0..SHRT_MAX
* effect{x,y,z} - effect direction
* name - sound file name
* Controller_Rumble:
* name = sound file name
* effect_volume = 0..USHRT_MAX
* effect_duration is in ms
* source = origin of audio
* from_player = if source is the client (player)
*/
void
Haptic_Feedback(char *name, int effect_volume, int effect_duration,
int effect_begin, int effect_end,
int effect_attack, int effect_fade,
int effect_x, int effect_y, int effect_z)
Controller_Rumble(const char *name, vec3_t source, qboolean from_player,
unsigned int duration, unsigned short int volume)
{
if (!joystick_haptic)
vec_t intens = 0.0f, low_freq = 1.0f, hi_freq = 1.0f, dist_prop;
unsigned short int max_distance = 4;
unsigned int effect_volume;
if (!show_haptic || !controller || joy_haptic_magnitude->value <= 0
|| volume == 0 || duration == 0)
{
return;
}
if (joy_haptic_magnitude->value <= 0)
if (strstr(name, "weapons/"))
{
intens = 1.75f;
if (strstr(name, "/blastf") || strstr(name, "/hyprbf") || strstr(name, "/nail"))
{
intens = 0.125f; // dampen blasters and nailgun's fire
low_freq = 0.7f;
hi_freq = 1.2f;
}
else if (strstr(name, "/shotgf") || strstr(name, "/rocklf"))
{
low_freq = 1.1f; // shotgun & RL shouldn't feel so weak
duration *= 0.7;
}
else if (strstr(name, "/sshotf"))
{
duration *= 0.6; // the opposite for super shotgun
}
else if (strstr(name, "/machgf") || strstr(name, "/disint"))
{
intens = 1.125f; // machine gun & disruptor fire
}
else if (strstr(name, "/grenlb") || strstr(name, "/hgrenb") // bouncing grenades
|| strstr(name, "open") || strstr(name, "warn")) // rogue's items
{
return; // ... don't have feedback
}
else if (strstr(name, "/plasshot")) // phalanx cannon
{
intens = 1.0f;
hi_freq = 0.3f;
duration *= 0.5;
}
else if (strstr(name, "x")) // explosions...
{
low_freq = 1.1f;
hi_freq = 0.9f;
max_distance = 550; // can be felt far away
}
else if (strstr(name, "r")) // reloads & ion ripper fire
{
low_freq = 0.1f;
hi_freq = 0.6f;
}
}
else if ( (strstr(name, "player/") || strstr(name, "players/")) &&
(strstr(name, "/death") || strstr(name, "/fall") || strstr(name, "/pain")) )
{
intens = 3.5f; // exaggerate player damage
low_freq = 1.1f;
}
else if (strstr(name, "player/land"))
{
intens = 2.5f; // fall without injury
}
else if (strstr(name, "doors/"))
{
intens = 0.125f;
low_freq = 0.4f;
max_distance = 280;
}
else if (strstr(name, "plats/"))
{
intens = 1.0f; // platforms rumble...
max_distance = 200; // when player near them
}
else if (strstr(name, "world/"))
{
max_distance = 3000; // ambient events
if (strstr(name, "/dish") || strstr(name, "/drill2a") || strstr(name, "/dr_")
|| strstr(name, "/explod1") || strstr(name, "/rocks") || strstr(name, "/rumble"))
{
intens = 0.25f;
low_freq = 0.7f;
}
else if (strstr(name, "/quake"))
{
intens = 0.625f; // (earth)quakes are more evident
low_freq = 1.2f;
}
}
if (intens == 0.0f)
{
return;
}
if (effect_volume <= 0)
if (from_player)
{
dist_prop = 1.0f;
}
else
{
dist_prop = VectorLength(source);
if (dist_prop > max_distance)
{
return;
}
if (effect_duration <= 0)
{
return;
dist_prop = (max_distance - dist_prop) / max_distance;
}
if (last_haptic_volume != (int)(joy_haptic_magnitude->value * 255))
{
IN_Haptic_Effects_Shutdown();
IN_Haptic_Effects_Init();
}
effect_volume = joy_haptic_magnitude->value * intens * dist_prop * volume;
low_freq = min(effect_volume * low_freq, USHRT_MAX);
hi_freq = min(effect_volume * hi_freq, USHRT_MAX);
last_haptic_volume = joy_haptic_magnitude->value * 255;
// Com_Printf("%s: vol %d | %d ms | prop:%.3f | l:%.0f h:%.0f\n",
// name, effect_volume, duration, dist_prop, low_freq, hi_freq);
if (
strstr(name, "misc/menu") ||
strstr(name, "weapons/") ||
/* detect pain for any player model */
((
strstr(name, "player/") ||
strstr(name, "players/")
) && (
strstr(name, "/pain")
)) ||
strstr(name, "player/step") ||
strstr(name, "player/land")
)
{
// check last effect for reuse
if (
last_haptic_efffect[last_haptic_efffect_pos].effect_volume != effect_volume ||
last_haptic_efffect[last_haptic_efffect_pos].effect_duration != effect_duration ||
last_haptic_efffect[last_haptic_efffect_pos].effect_begin != effect_begin ||
last_haptic_efffect[last_haptic_efffect_pos].effect_end != effect_end ||
last_haptic_efffect[last_haptic_efffect_pos].effect_attack != effect_attack ||
last_haptic_efffect[last_haptic_efffect_pos].effect_fade != effect_fade ||
last_haptic_efffect[last_haptic_efffect_pos].effect_x != effect_x ||
last_haptic_efffect[last_haptic_efffect_pos].effect_y != effect_y ||
last_haptic_efffect[last_haptic_efffect_pos].effect_z != effect_z)
{
if ((SDL_HapticQuery(joystick_haptic) & SDL_HAPTIC_SINE)==0)
{
return;
}
int hapric_volume = joy_haptic_magnitude->value * effect_volume; // 32767 max strength;
if (effect_duration <= 0)
{
return;
}
/*
Com_Printf("%s: volume %d: %d ms %d:%d:%d ms speed: %.2f\n",
name, effect_volume, effect_duration - effect_end,
effect_begin, effect_attack, effect_fade,
(float)effect_volume / effect_fade);
*/
// FIFO for effects
last_haptic_efffect_pos = (last_haptic_efffect_pos+1) % last_haptic_efffect_size;
IN_Haptic_Effect_Shutdown(&last_haptic_efffect[last_haptic_efffect_pos].effect_id);
last_haptic_efffect[last_haptic_efffect_pos].effect_volume = effect_volume;
last_haptic_efffect[last_haptic_efffect_pos].effect_duration = effect_duration;
last_haptic_efffect[last_haptic_efffect_pos].effect_attack = effect_attack;
last_haptic_efffect[last_haptic_efffect_pos].effect_fade = effect_fade;
last_haptic_efffect[last_haptic_efffect_pos].effect_x = effect_x;
last_haptic_efffect[last_haptic_efffect_pos].effect_y = effect_y;
last_haptic_efffect[last_haptic_efffect_pos].effect_z = effect_z;
last_haptic_efffect[last_haptic_efffect_pos].effect_id = IN_Haptic_Effect_Init(
effect_x, effect_y, effect_z,
effect_duration - effect_end, hapric_volume,
effect_begin, effect_attack, effect_fade);
}
SDL_HapticRunEffect(joystick_haptic, last_haptic_efffect[last_haptic_efffect_pos].effect_id, 1);
}
SDL_GameControllerRumble(controller, low_freq, hi_freq, duration);
}
/*
@ -1673,7 +1558,7 @@ IN_Controller_Init(qboolean notify_user)
Com_Printf("- Game Controller init attempt -\n");
}
if (!SDL_WasInit(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC))
if (!SDL_WasInit(SDL_INIT_GAMECONTROLLER))
{
#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE // extended input reports on PS controllers (enables gyro thru bluetooth)
@ -1683,7 +1568,7 @@ IN_Controller_Init(qboolean notify_user)
SDL_SetHint( SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1" );
#endif
if (SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) == -1)
if (SDL_Init(SDL_INIT_GAMECONTROLLER) == -1)
{
Com_Printf ("Couldn't init SDL joystick: %s.\n", SDL_GetError ());
return;
@ -1694,17 +1579,6 @@ IN_Controller_Init(qboolean notify_user)
if (!SDL_NumJoysticks())
{
joystick_haptic = SDL_HapticOpenFromMouse();
if (joystick_haptic == NULL)
{
Com_Printf("Most likely mouse isn't haptic.\n");
}
else
{
IN_Haptic_Effects_Info();
}
return;
}
@ -1770,17 +1644,6 @@ IN_Controller_Init(qboolean notify_user)
Com_Printf (" * snap-to-axis ratio = %.3f\n", joy_right_snapaxis->value);
Com_Printf (" * inner deadzone = %.3f\n", joy_right_deadzone->value);
joystick_haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(controller));
if (joystick_haptic == NULL)
{
Com_Printf("Most likely controller isn't haptic.\n");
}
else
{
IN_Haptic_Effects_Info();
}
#if SDL_VERSION_ATLEAST(2, 0, 16) // support for controller sensors
if ( SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO)
@ -1810,6 +1673,22 @@ IN_Controller_Init(qboolean notify_user)
#endif // SDL_VERSION_ATLEAST(2, 0, 16)
#if SDL_VERSION_ATLEAST(2, 0, 18) // support for query on features from controller
if (SDL_GameControllerHasRumble(controller))
{
show_haptic = true;
Com_Printf("Rumble support available.\n");
}
else
{
show_haptic = false;
Com_Printf("Controller doesn't support rumble.\n");
}
#else
show_haptic = true; // when in doubt, say yes
Com_Printf("Controller might support rumble.\n");
#endif // SDL_VERSION_ATLEAST(2, 0, 18)
break;
}
}
@ -1887,21 +1766,6 @@ IN_Init(void)
Com_Printf("------------------------------------\n\n");
}
/*
* Shuts the backend down
*/
static void
IN_Haptic_Shutdown(void)
{
if (joystick_haptic)
{
IN_Haptic_Effects_Shutdown();
SDL_HapticClose(joystick_haptic);
joystick_haptic = NULL;
}
}
static void
IN_Controller_Shutdown(qboolean notify_user)
{
@ -1910,8 +1774,6 @@ IN_Controller_Shutdown(qboolean notify_user)
Com_Printf("- Game Controller disconnected -\n");
}
IN_Haptic_Shutdown();
if (controller)
{
SDL_GameControllerClose(controller);
@ -1923,6 +1785,9 @@ IN_Controller_Shutdown(qboolean notify_user)
}
}
/*
* Shuts the backend down
*/
void
IN_Shutdown(void)
{
@ -1939,7 +1804,7 @@ IN_Shutdown(void)
IN_Controller_Shutdown(false);
const Uint32 subsystems = SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC;
const Uint32 subsystems = SDL_INIT_GAMECONTROLLER;
if (SDL_WasInit(subsystems) == subsystems)
SDL_QuitSubSystem(subsystems);
}

View file

@ -1942,10 +1942,10 @@ Joy_MenuInit(void)
s_joy_haptic_slider.generic.x = 0;
s_joy_haptic_slider.generic.y = y;
y += 10;
s_joy_haptic_slider.generic.name = "haptic magnitude";
s_joy_haptic_slider.generic.name = "rumble intensity";
s_joy_haptic_slider.cvar = "joy_haptic_magnitude";
s_joy_haptic_slider.minvalue = 0.0f;
s_joy_haptic_slider.maxvalue = 2.2f;
s_joy_haptic_slider.maxvalue = 2.0f;
Menu_AddItem(&s_joy_menu, (void *)&s_joy_haptic_slider);
}

View file

@ -1109,34 +1109,18 @@ S_StartSound(vec3_t origin, int entnum, int entchannel, sfx_t *sfx,
if (sfx->name[0])
{
vec3_t orientation, direction;
vec_t distance_direction;
int dir_x, dir_y, dir_z;
int effect_duration = 0;
int effect_volume = -1;
vec3_t direction = {0};
unsigned int effect_duration = 0;
unsigned short int effect_volume = 0;
VectorSubtract(listener_forward, listener_up, orientation);
// with !fixed we have all sounds related directly to player,
// with !ps->fixed we have all sounds related directly to player,
// e.g. players fire, pain, menu
if (!ps->fixed_origin)
{
VectorCopy(orientation, direction);
distance_direction = 0;
}
else
// else, they come from the environment
if (ps->fixed_origin)
{
VectorSubtract(listener_origin, ps->origin, direction);
distance_direction = VectorLength(direction);
}
VectorNormalize(direction);
VectorNormalize(orientation);
dir_x = 16 * orientation[0] * direction[0];
dir_y = 16 * orientation[1] * direction[1];
dir_z = 16 * orientation[2] * direction[2];
if (sfx->cache)
{
effect_duration = sfx->cache->length;
@ -1146,16 +1130,11 @@ S_StartSound(vec3_t origin, int entnum, int entchannel, sfx_t *sfx,
effect_duration /= 2;
}
/* sound near player has 16 points */
effect_volume = sfx->cache->volume / 16;
effect_volume = sfx->cache->volume;
}
Haptic_Feedback(
sfx->name, (16 - distance_direction / 32) * effect_volume,
effect_duration,
sfx->cache->begin, sfx->cache->end,
sfx->cache->attack, sfx->cache->fade,
dir_x, dir_y, dir_z);
Controller_Rumble(sfx->name, direction, !ps->fixed_origin,
effect_duration, effect_volume);
}
ps->entnum = entnum;