Unify thumbsticks

- Combined the functions that handle dead zone, digital threshold, and response curves together.
  - A lot less copy-paste between the different platforms.
- Combine the X and Y settings for dead zone, digital threshold, and response curve for analog sticks. This fixes many issues:
  - The dead zone shape for sticks are now a true circle, instead of being axial.
  - The new response curves work better when moving diagonally. Previously, it'd nerf your move speed up to ~60-50% if you had it on anything but Linear.
- Refactored the new POSIX SDL gamepad axis code to work more like Windows' XInput code. This comes with the following benefits:
  - Right thumbstick should function now.
  - Left thumbstick now sends the gamepad keycodes instead of raw joystick keycodes.
- Added `stat analog`, to display current movement analog stick values. (I've needed it a lot lately.)
This commit is contained in:
Sally Coolatta 2025-07-11 00:17:01 -04:00 committed by Rachael Alexanderson
commit e1a5326947
8 changed files with 442 additions and 198 deletions

View file

@ -475,40 +475,6 @@ CCMD (gamepad)
return usage();
}
//===========================================================================
//
// Joy_RemoveDeadZone
//
//===========================================================================
double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons)
{
uint8_t butt;
// Cancel out deadzone.
if (fabs(axisval) < deadzone)
{
axisval = 0;
butt = 0;
}
// Make the dead zone the new 0.
else if (axisval < 0)
{
axisval = (axisval + deadzone) / (1.0 - deadzone);
butt = 2; // button minus
}
else
{
axisval = (axisval - deadzone) / (1.0 - deadzone);
butt = 1; // button plus
}
if (buttons != NULL)
{
*buttons = butt;
}
return axisval;
}
//===========================================================================
//
// Joy_ApplyResponseCurveBezier
@ -551,6 +517,54 @@ double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input)
return sign*t;
}
//===========================================================================
//
// Joy_ManageSingleAxis
//
//===========================================================================
double Joy_ManageSingleAxis(double axisval, double deadzone, double threshold, const CubicBezier &curve, uint8_t *buttons)
{
uint8_t butt;
// Cancel out deadzone.
if (fabs(axisval) < deadzone)
{
axisval = 0;
butt = 0;
}
else
{
// Make the dead zone the new 0.
if (axisval < 0)
{
axisval = (axisval + deadzone) / (1.0 - deadzone);
butt = 2; // button minus
}
else
{
axisval = (axisval - deadzone) / (1.0 - deadzone);
butt = 1; // button plus
}
// Apply input response curve
axisval = Joy_ApplyResponseCurveBezier(curve, axisval);
if (abs(axisval) < threshold)
{
// Needs to meet digital threshold to send button
butt = 0;
}
}
if (buttons != NULL)
{
*buttons = butt;
}
return axisval;
}
//===========================================================================
//
// Joy_XYAxesToButtons
@ -588,6 +602,86 @@ int Joy_XYAxesToButtons(double x, double y)
return JoyAngleButtons[int(rad) & 7];
}
//===========================================================================
//
// Joy_ManageThumbstick
//
// Given two axes and their settings, handle applying deadzone, digital
// threshold, input response curve, and setting button state. This is
// necessary, because doing the two axes individually creates awkward
// behavior (such as cross shaped deadzones, sluggish input response when
// pressing diagonally, so on).
//
//===========================================================================
double Joy_ManageThumbstick(
double *axis_x, double *axis_y,
double deadzone_x, double deadzone_y,
double threshold_x, double threshold_y,
const CubicBezier &curve_x, const CubicBezier &curve_y,
uint8_t *buttons)
{
double ret_x = *axis_x;
double ret_y = *axis_y;
double x_abs = abs(*axis_x);
double y_abs = abs(*axis_y);
double magnitude = sqrt((x_abs * x_abs) + (y_abs * y_abs));
double ret_dist = 0;
uint8_t ret_butt = 0;
double xy_lerp = 0.5;
if (magnitude > 0)
{
// Later it would be nice to have a single deadzone / threshold / curve setting
// for the whole thumbstick instead of awkwardly trying to combine them, but
// that requires re-considering how axes are exposed to the rest of the engine.
// This will do for now.
xy_lerp = abs(cos(atan2(ret_y, ret_x)));
}
double deadzone = (xy_lerp * deadzone_x) + ((1.0 - xy_lerp) * deadzone_y);
if (magnitude < deadzone)
{
ret_x = 0;
ret_y = 0;
}
else
{
// Make the dead zone the new 0.
ret_dist = (magnitude - deadzone) / (1.0 - deadzone);
const CubicBezier curve = {
(float)((xy_lerp * curve_x.x1) + ((1.0 - xy_lerp) * curve_y.x1)),
(float)((xy_lerp * curve_x.y1) + ((1.0 - xy_lerp) * curve_y.y1)),
(float)((xy_lerp * curve_x.x2) + ((1.0 - xy_lerp) * curve_y.x2)),
(float)((xy_lerp * curve_x.y2) + ((1.0 - xy_lerp) * curve_y.y2))
};
ret_dist = Joy_ApplyResponseCurveBezier(curve, ret_dist);
ret_x = (ret_x / magnitude) * ret_dist;
ret_y = (ret_y / magnitude) * ret_dist;
double threshold = (xy_lerp * threshold_x) + ((1.0 - xy_lerp) * threshold_y);
if (ret_dist >= threshold)
{
ret_butt = Joy_XYAxesToButtons(ret_x, ret_y);
}
}
*axis_x = ret_x;
*axis_y = ret_y;
if (buttons != NULL)
{
*buttons = ret_butt;
}
return ret_dist;
}
//===========================================================================
//
// Joy_GenerateButtonEvent

View file

@ -88,9 +88,13 @@ void M_SaveJoystickConfig(IJoystickConfig *joy);
void Joy_GenerateButtonEvent(bool down, EKeyCodes which);
void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, int base);
void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, const int *keys);
int Joy_XYAxesToButtons(double x, double y);
double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons);
double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input);
double Joy_ManageSingleAxis(double axisval, double deadzone, double threshold, const CubicBezier &curve, uint8_t *buttons);
int Joy_XYAxesToButtons(double x, double y);
double Joy_ManageThumbstick(double *axis_x, double *axis_y, double deadzone_x, double deadzone_y,
double threshold_x, double threshold_y, const CubicBezier &curve_x, const CubicBezier &curve_y, uint8_t *buttons);
// These ought to be provided by a system-specific i_input.cpp.
void I_GetAxes(float axes[NUM_AXIS_CODES]);

View file

@ -638,40 +638,80 @@ void IOKitJoystick::ProcessAxes()
for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
{
AnalogAxis& axis = m_axes[i];
uint8_t buttonstate = 0;
static const double scaledMin = -1;
static const double scaledMax = 1;
IOHIDEventStruct event;
if (kIOReturnSuccess == (*m_interface)->getElementValue(m_interface, axis.cookie, &event))
{
const double scaledValue = scaledMin +
(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, &buttonstate);
const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue);
axis.value = static_cast<float>(smoothedValue * m_sensitivity * axis.sensitivity);
}
else
{
axis.value = 0.0f;
}
if (i < NUM_JOYAXISBUTTONS && (i > 2 || m_axes.Size() == 1))
{
AnalogAxis& axis = m_axes[i];
uint8_t buttonstate = 0;
IOHIDEventStruct event;
if (kIOReturnSuccess == (*m_interface)->getElementValue(m_interface, axis.cookie, &event))
{
const double scaledValue = scaledMin +
(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
const double filteredValue = Joy_ManageSingleAxis(
scaledValue,
axis.deadZone,
axis.digitalThreshold,
axis.responseCurve,
&buttonstate
);
axis.value = static_cast<float>(filteredValue * m_sensitivity * axis.sensitivity);
}
else
{
axis.value = 0.0f;
}
Joy_GenerateButtonEvents(axis.buttonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
axis.buttonValue = buttonstate;
}
else if (i == 1)
{
// Since we sorted the axes, we know that the first two are definitely X and Y.
// They are probably a single stick, so use angular position to determine buttons.
buttonstate = Joy_XYAxesToButtons(m_axes[0].value, axis.value);
Joy_GenerateButtonEvents(axis.buttonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
// They are probably a single stick, so treat them as one.
AnalogAxis& axis_x = m_axes[i - 1];
AnalogAxis& axis_y = m_axes[i];
uint8_t buttonstate = 0;
double axisval_x = 0.0f;
double axisval_y = 0.0f;
IOHIDEventStruct event;
if (kIOReturnSuccess == (*m_interface)->getElementValue(m_interface, axis_x.cookie, &event))
{
axisval_x = scaledMin +
(event.value - axis_x.minValue) * (scaledMax - scaledMin) / (axis_x.maxValue - axis_x.minValue);
}
if (kIOReturnSuccess == (*m_interface)->getElementValue(m_interface, axis_y.cookie, &event))
{
axisval_y = scaledMin +
(event.value - axis_y.minValue) * (scaledMax - scaledMin) / (axis_y.maxValue - axis_y.minValue);
}
Joy_ManageThumbstick(
&axisval_x, &axisval_y,
axis_x.deadZone, axis_y.deadZone,
axis_x.digitalThreshold, axis_y.digitalThreshold,
axis_x.responseCurve, axis_y.responseCurve,
&buttonstate
);
axis_x.value = static_cast<float>(axisval_x * m_sensitivity * axis_x.sensitivity);
axis_y.value = static_cast<float>(axisval_y * m_sensitivity * axis_y.sensitivity);
// We store all four buttons in the first axis and ignore the second.
Joy_GenerateButtonEvents(axis_x.buttonValue, buttonstate, 4, KEY_JOYAXIS1PLUS + (i-1)*2);
axis_x.buttonValue = buttonstate;
}
axis.buttonValue = buttonstate;
}
}
@ -698,10 +738,16 @@ bool IOKitJoystick::ProcessAxis(const IOHIDEventStruct& event)
const double scaledValue = scaledMin +
(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, &buttonstate);
const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue);
axis.value = static_cast<float>(smoothedValue * m_sensitivity * axis.sensitivity);
const double filteredValue = Joy_ManageSingleAxis(
scaledValue,
axis.deadZone,
axis.digitalThreshold,
axis.responseCurve,
&buttonstate
);
axis.value = static_cast<float>(filteredValue * m_sensitivity * axis.sensitivity);
if (i < NUM_JOYAXISBUTTONS)
{

View file

@ -41,6 +41,16 @@
#include "i_input.h"
#include "m_joy.h"
static const EAxisCodes ControllerAxisCodes[][2] =
{
{ AXIS_CODE_PAD_LTHUMB_RIGHT, AXIS_CODE_PAD_LTHUMB_LEFT },
{ AXIS_CODE_PAD_LTHUMB_DOWN, AXIS_CODE_PAD_LTHUMB_UP },
{ AXIS_CODE_PAD_RTHUMB_RIGHT, AXIS_CODE_PAD_RTHUMB_LEFT },
{ AXIS_CODE_PAD_RTHUMB_DOWN, AXIS_CODE_PAD_RTHUMB_UP },
{ AXIS_CODE_PAD_LTRIGGER, AXIS_CODE_NULL },
{ AXIS_CODE_PAD_RTRIGGER, AXIS_CODE_NULL }
};
class SDLInputJoystick: public IJoystickConfig
{
public:
@ -283,147 +293,200 @@ public:
return id;
}
void AddAxes(float axes[NUM_AXIS_CODES])
void AddAxes(float joyaxes[NUM_AXIS_CODES])
{
// Add to game axes.
for (int i = 0; i < GetNumAxes(); ++i)
{
float axis_value = float(Axes[i].Value * Multiplier * Axes[i].Multiplier);
int code = AXIS_CODE_NULL;
int axis_code_pos = AXIS_CODE_NULL;
int axis_code_neg = AXIS_CODE_NULL;
int ret_code = AXIS_CODE_NULL;
if (Mapping)
{
if (i < SDL_CONTROLLER_AXIS_MAX)
{
axis_code_pos = ControllerAxisCodes[i][0];
axis_code_neg = ControllerAxisCodes[i][1];
}
}
else
{
if (i < NUM_JOYAXISBUTTONS)
{
axis_code_pos = AXIS_CODE_JOY1_PLUS + (i * 2);
axis_code_neg = AXIS_CODE_JOY1_PLUS + (i * 2) + 1;
}
}
if (axis_value > 0.0f)
{
code = Axes[i].Codes[0];
ret_code = axis_code_pos;
}
else if (axis_value < 0.0f)
{
code = Axes[i].Codes[1];
ret_code = axis_code_neg;
}
if (code != AXIS_CODE_NULL)
if (ret_code != AXIS_CODE_NULL)
{
axes[code] += fabs(axis_value);
joyaxes[ret_code] += fabs(axis_value);
}
}
}
void ProcessGameControllerThumbstick(int index1, int index2, int base)
{
AxisInfo &axis1 = Axes[index1];
AxisInfo &axis2 = Axes[index2];
uint8_t buttonstate;
double axisval1, axisval2;
axisval1 = SDL_GameControllerGetAxis(Mapping, static_cast<SDL_GameControllerAxis>(index1)) / 32767.0;
axisval2 = SDL_GameControllerGetAxis(Mapping, static_cast<SDL_GameControllerAxis>(index2)) / 32767.0;
Joy_ManageThumbstick(
&axisval1, &axisval2,
axis1.DeadZone, axis2.DeadZone,
axis1.DigitalThreshold, axis2.DigitalThreshold,
axis1.ResponseCurve, axis2.ResponseCurve,
&buttonstate
);
axis1.Value = float(axisval1);
axis2.Value = float(axisval2);
// We store all four buttons in the first axis and ignore the second.
Joy_GenerateButtonEvents(axis1.ButtonValue, buttonstate, 4, base);
axis1.ButtonValue = buttonstate;
}
void ProcessGameControllerTrigger(int index, int base)
{
AxisInfo &axis = Axes[index];
uint8_t buttonstate;
double axisval;
axisval = SDL_GameControllerGetAxis(Mapping, static_cast<SDL_GameControllerAxis>(index)) / 32767.0;
axisval = Joy_ManageSingleAxis(
axisval,
axis.DeadZone, axis.DigitalThreshold, axis.ResponseCurve,
&buttonstate
);
axis.Value = float(axisval);
Joy_GenerateButtonEvents(axis.ButtonValue, buttonstate, 1, base);
axis.ButtonValue = buttonstate;
}
void ProcessGameControllerAxes()
{
// Refactored to match more closely with XInput / raw PS2 on Windows
ProcessGameControllerThumbstick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, KEY_PAD_LTHUMB_RIGHT);
ProcessGameControllerThumbstick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, KEY_PAD_RTHUMB_RIGHT);
ProcessGameControllerTrigger(SDL_CONTROLLER_AXIS_TRIGGERLEFT, KEY_PAD_LTRIGGER);
ProcessGameControllerTrigger(SDL_CONTROLLER_AXIS_TRIGGERRIGHT, KEY_PAD_RTRIGGER);
}
void ProcessOldJoystickAxes()
{
uint8_t buttonstate;
for (int i = 0; i < NumAxes; ++i)
{
buttonstate = 0;
if (i < NUM_JOYAXISBUTTONS && (i > 2 || Axes.Size() == 1))
{
AxisInfo &info = Axes[i];
double axisval;
axisval = SDL_JoystickGetAxis(Device, i) / 32767.0;
info.Value = Joy_ManageSingleAxis(
axisval,
info.DeadZone,
info.DigitalThreshold,
info.ResponseCurve,
&buttonstate
);
Joy_GenerateButtonEvents(info.ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
info.ButtonValue = buttonstate;
}
else if (i == 1)
{
AxisInfo &info_x = Axes[i - 1];
AxisInfo &info_y = Axes[i];
double axisval_x, axisval_y;
axisval_x = SDL_JoystickGetAxis(Device, i - 1) / 32767.0;
axisval_y = SDL_JoystickGetAxis(Device, i) / 32767.0;
Joy_ManageThumbstick(
&axisval_x, &axisval_y,
info_x.DeadZone, info_y.DeadZone,
info_x.DigitalThreshold, info_y.DigitalThreshold,
info_x.ResponseCurve, info_y.ResponseCurve,
&buttonstate
);
info_x.Value = axisval_x;
info_y.Value = axisval_y;
Joy_GenerateButtonEvents(info_x.ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS + (i-1)*2);
info_x.ButtonValue = buttonstate;
}
}
// Map POV hats to buttons and axes. Why axes? Well apparently I have
// a gamepad where the left control stick is a POV hat (instead of the
// d-pad like you would expect, no that's pressure sensitive). Also
// KDE's joystick dialog maps them to axes as well.
for (int i = 0; i < NumHats; ++i)
{
AxisInfo &x = Axes[NumAxes + i*2];
AxisInfo &y = Axes[NumAxes + i*2 + 1];
buttonstate = SDL_JoystickGetHat(Device, i);
// If we're going to assume that we can pass SDL's value into
// Joy_GenerateButtonEvents then we might as well assume the format here.
if(buttonstate & 0x1) // Up
y.Value = -1.0;
else if(buttonstate & 0x4) // Down
y.Value = 1.0;
else
y.Value = 0.0;
if(buttonstate & 0x2) // Left
x.Value = 1.0;
else if(buttonstate & 0x8) // Right
x.Value = -1.0;
else
x.Value = 0.0;
if (i < 4)
{
Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4);
x.ButtonValue = buttonstate;
}
}
}
void ProcessInput()
{
uint8_t buttonstate;
if (Mapping)
{
// GameController API available
auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold;
auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold;
for (auto i = 0; i < SDL_CONTROLLER_AXIS_MAX && i < NumAxes; ++i)
{
buttonstate = 0;
if (i < NUM_JOYAXISBUTTONS)
{
Axes[i].Codes[0] = AXIS_CODE_JOY1_PLUS + (i * 2);
Axes[i].Codes[1] = AXIS_CODE_JOY1_PLUS + (i * 2) + 1;
}
else
{
Axes[i].Codes[0] = Axes[i].Codes[1] = AXIS_CODE_NULL;
}
Axes[i].Value = SDL_GameControllerGetAxis(Mapping, static_cast<SDL_GameControllerAxis>(i))/32767.0;
Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value);
}
auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold;
auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold;
if (lastTriggerL != currTriggerL) Joy_GenerateButtonEvent(currTriggerL, KEY_PAD_LTRIGGER);
if (lastTriggerR != currTriggerR) Joy_GenerateButtonEvent(currTriggerR, KEY_PAD_RTRIGGER);
// todo: right stick
buttonstate = Joy_XYAxesToButtons(
abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value,
abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value
);
Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
Axes[0].ButtonValue = buttonstate;
ProcessGameControllerAxes();
}
else
{
// Joystick API fallback
for (int i = 0; i < NumAxes; ++i)
{
buttonstate = 0;
if (i < NUM_JOYAXISBUTTONS)
{
Axes[i].Codes[0] = AXIS_CODE_JOY1_PLUS + (i * 2);
Axes[i].Codes[1] = AXIS_CODE_JOY1_PLUS + (i * 2) + 1;
}
else
{
Axes[i].Codes[0] = Axes[i].Codes[1] = AXIS_CODE_NULL;
}
Axes[i].Value = SDL_JoystickGetAxis(Device, i)/32767.0;
Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value);
// Map button to axis
// X and Y are handled differently so if we have 2 or more axes then we'll use that code instead.
if (NumAxes == 1 || (i >= 2 && i < NUM_JOYAXISBUTTONS))
{
Joy_GenerateButtonEvents(Axes[i].ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
Axes[i].ButtonValue = buttonstate;
}
}
if (NumAxes > 1)
{
buttonstate = Joy_XYAxesToButtons(
abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value,
abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value
);
Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
Axes[0].ButtonValue = buttonstate;
}
// Map POV hats to buttons and axes. Why axes? Well apparently I have
// a gamepad where the left control stick is a POV hat (instead of the
// d-pad like you would expect, no that's pressure sensitive). Also
// KDE's joystick dialog maps them to axes as well.
for (int i = 0; i < NumHats; ++i)
{
AxisInfo &x = Axes[NumAxes + i*2];
AxisInfo &y = Axes[NumAxes + i*2 + 1];
buttonstate = SDL_JoystickGetHat(Device, i);
// If we're going to assume that we can pass SDL's value into
// Joy_GenerateButtonEvents then we might as well assume the format here.
if(buttonstate & 0x1) // Up
y.Value = -1.0;
else if(buttonstate & 0x4) // Down
y.Value = 1.0;
else
y.Value = 0.0;
if(buttonstate & 0x2) // Left
x.Value = 1.0;
else if(buttonstate & 0x8) // Right
x.Value = -1.0;
else
x.Value = 0.0;
if (i < 4)
{
Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4);
x.ButtonValue = buttonstate;
}
}
ProcessOldJoystickAxes();
}
}
@ -436,7 +499,6 @@ protected:
float DigitalThreshold;
EJoyCurve ResponseCurvePreset;
CubicBezier ResponseCurve;
int Codes[2]; // 0: positive, 1: negative
double Value;
uint8_t ButtonValue;
};

View file

@ -451,33 +451,58 @@ void FDInputJoystick::ProcessInput()
// into button presses.
for (i = 0; i < Axes.Size(); ++i)
{
AxisInfo *info = &Axes[i];
LONG value = *(LONG *)(state + info->Ofs);
double axisval;
uint8_t buttonstate = 0;
// Scale to [-1.0, 1.0]
axisval = (value - info->Min) * 2.0 / (info->Max - info->Min) - 1.0;
// Cancel out dead zone
axisval = Joy_RemoveDeadZone(axisval, info->DeadZone, &buttonstate);
axisval = Joy_ApplyResponseCurveBezier(info->ResponseCurve, axisval);
info->Value = float(axisval);
if (i < NUM_JOYAXISBUTTONS && (i > 2 || Axes.Size() == 1))
{
if (abs(axisval) < info->DigitalThreshold) buttonstate = 0;
AxisInfo *info = &Axes[i];
LONG value = *(LONG *)(state + info->Ofs);
double axisval;
// Scale to [-1.0, 1.0]
axisval = (value - info->Min) * 2.0 / (info->Max - info->Min) - 1.0;
// Cancel out dead zone
axisval = Joy_ManageSingleAxis(
axisval,
info->DeadZone,
info->DigitalThreshold,
info->ResponseCurve,
&buttonstate
);
Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
info->ButtonValue = buttonstate;
}
else if (i == 1)
{
// Since we sorted the axes, we know that the first two are definitely X and Y.
// They are probably a single stick, so use angular position to determine buttons.
buttonstate = Joy_XYAxesToButtons(
(abs(Axes[0].Value) < Axes[0].DigitalThreshold) ? 0 : Axes[0].Value,
(abs(axisval) < info->DigitalThreshold) ? 0 : axisval
// They are probably a single stick, so treat them as one.
AxisInfo *info_x = &Axes[i - 1];
AxisInfo *info_y = &Axes[i];
LONG value_x = *(LONG *)(state + info_x->Ofs);
LONG value_y = *(LONG *)(state + info_y->Ofs);
double axisval_x, axisval_y;
// Scale to [-1.0, 1.0]
axisval_x = (value_x - info_x->Min) * 2.0 / (info_x->Max - info_x->Min) - 1.0;
axisval_y = (value_y - info_y->Min) * 2.0 / (info_y->Max - info_y->Min) - 1.0;
// Cancel out dead zone
Joy_ManageThumbstick(
&axisval_x, &axisval_y,
info_x->DeadZone, info_y->DeadZone,
info_x->DigitalThreshold, info_y->DigitalThreshold,
info_x->ResponseCurve, info_y->ResponseCurve,
&buttonstate
);
Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
// We store all four buttons in the first axis and ignore the second.
Joy_GenerateButtonEvents(info_x->ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS + (i-1)*2);
info_x->ButtonValue = buttonstate;
}
info->ButtonValue = buttonstate;
}
// Compare button states and generate events for buttons that have changed.

View file

@ -565,18 +565,19 @@ void FRawPS2Controller::ProcessThumbstick(int value1, AxisInfo *axis1, int value
axisval1 = value1 * (2.0 / 255) - 1.0;
axisval2 = value2 * (2.0 / 255) - 1.0;
axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL);
axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL);
axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1);
axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2);
Joy_ManageThumbstick(
&axisval1, &axisval2,
axis1->DeadZone, axis2->DeadZone,
axis1->DigitalThreshold, axis2->DigitalThreshold,
axis1->ResponseCurve, axis2->ResponseCurve,
&buttonstate
);
axis1->Value = float(axisval1);
axis2->Value = float(axisval2);
// We store all four buttons in the first axis and ignore the second.
buttonstate = Joy_XYAxesToButtons(
(abs(axisval1) < axis1->DigitalThreshold) ? 0 : axisval1,
(abs(axisval2) < axis2->DigitalThreshold) ? 0 : axisval2
);
Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base);
axis1->ButtonValue = buttonstate;
}

View file

@ -343,18 +343,19 @@ void FXInputController::ProcessThumbstick(int value1, AxisInfo *axis1,
axisval1 = (value1 - SHRT_MIN) * 2.0 / 65536 - 1.0;
axisval2 = (value2 - SHRT_MIN) * 2.0 / 65536 - 1.0;
axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL);
axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL);
axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1);
axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2);
Joy_ManageThumbstick(
&axisval1, &axisval2,
axis1->DeadZone, axis2->DeadZone,
axis1->DigitalThreshold, axis2->DigitalThreshold,
axis1->ResponseCurve, axis2->ResponseCurve,
&buttonstate
);
axis1->Value = float(axisval1);
axis2->Value = float(axisval2);
// We store all four buttons in the first axis and ignore the second.
buttonstate = Joy_XYAxesToButtons(
abs(axisval1) < axis1->DigitalThreshold ? 0: axisval1,
abs(axisval2) < axis2->DigitalThreshold ? 0: axisval2
);
Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base);
axis1->ButtonValue = buttonstate;
}
@ -373,15 +374,15 @@ void FXInputController::ProcessTrigger(int value, AxisInfo *axis, int base)
uint8_t buttonstate;
double axisval;
axisval = Joy_RemoveDeadZone(value / 256.0, axis->DeadZone, &buttonstate);
axisval = Joy_ApplyResponseCurveBezier(axis->ResponseCurve, axisval);
// TODO: probably just put this into Joy_RemoveDeadZone
if (abs(axisval) < axis->DigitalThreshold) buttonstate = 0;
axisval = Joy_ManageSingleAxis(
value / 256.0,
axis->DeadZone, axis->DigitalThreshold, axis->ResponseCurve,
&buttonstate
);
axis->Value = float(axisval);
Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 1, base);
axis->ButtonValue = buttonstate;
axis->Value = float(axisval);
}
//==========================================================================

View file

@ -825,6 +825,17 @@ void G_BuildTiccmd (usercmd_t *cmd)
cmd->sidemove <<= 8;
}
ADD_STAT (analog)
{
FString out;
float axis_forward = buttonMap.ButtonAnalog(Button_Forward) - buttonMap.ButtonAnalog(Button_Back);
float axis_side = buttonMap.ButtonAnalog(Button_MoveLeft) - buttonMap.ButtonAnalog(Button_MoveRight);
out.AppendFormat("[%.3f, %.3f]", axis_forward, axis_side);
return out;
}
static int LookAdjust(int look)
{
look <<= 16;