How to calculate joystick sensitivity, taking into account deadzone and the circular nature of the stick?
I'm working on a class that represents a stick of a gamepad. I'm having trouble with the mathematics of it, specifically with the sensitivity part. Sensitivity should make the joystick's distance from center non-linear. I applied sensitivity on a X-Box trigger without problems, but because a joystick has two axis (X and Y), I'm having trouble with the math involved.
I want to apply circular sensitivity to the stick, but I don't really know how to do that, specially taking into account other calculations on the axes (like deadzone, distance from center, etc.). How sould I accomplish that?
Additional details about the problem
Right now, I already have my temporary fix which is not working very well. It seems to be working when the joystick direction is either horizontal or vertical, but when I move it to a diagonal direction, is seems buged. My Joystick
class has a Distance
property, which retrieves the stick's distance from center (a value from 0 to 1). My Distance
property is working well, but when I apply the sensitivity, the retrieved distance is less than 1 on diagonal directions if I move my josytick around, when it should be exactly 1, no matter the direction.
Below, I'm including a simplified version of my Joystick
class, where I removed most of the unrelevant code. The calculated X and Y positions of the axes are retrieved by ComputedX
and ComputedY
properties. Each of this properties should include its axis final position (from -1 to 1) taking into account all the modifiers (deadzone, saturation, sensitivity, etc.).
public class Joystick
{
// Properties
// Physical axis positions
public double X { get; set;}
public double Y { get; set; }
// Virtual axis positions, with all modifiers applied (like deadzone, sensitivity, etc.)
public double ComputedX { get => ComputeX(); }
public double ComputedY {get => ComputeY(); }
// Joystick modifiers, which influence the computed axis positions
public double DeadZone { get; set; }
public double Saturation { get; set; }
public double Sensitivity { get; set; }
public double Range { get; set; }
public bool InvertX { get; set; }
public bool InvertY { get; set; }
// Other properties
public double Distance
{
get => CoerceValue(Math.Sqrt((ComputedX * ComputedX) + (ComputedY * ComputedY)), 0d, 1d);
}
public double Direction { get => ComputeDirection(); }
// Methods
private static double CoerceValue(double value, double minValue, double maxValue)
{
return (value < minValue) ? minValue : ((value > maxValue) ? maxValue : value);
}
protected virtual double ComputeX()
{
double value = X;
value = CalculateDeadZoneAndSaturation(value, DeadZone, Saturation);
value = CalculateSensitivity(value, Sensitivity);
value = CalculateRange(value, Range);
if (InvertX) value = -value;
return CoerceValue(value, -1d, 1d);
}
protected virtual double ComputeY()
{
double value = Y;
value = CalculateDeadZoneAndSaturation(value, DeadZone, Saturation);
value = CalculateSensitivity(value, Sensitivity);
value = CalculateRange(value, Range);
if (InvertY) value = -value;
return CoerceValue(value, -1d, 1d);
}
/// <sumary>Gets the joystick's direction (from 0 to 1).</summary>
private double ComputeDirection()
{
double x = ComputedX;
double y = ComputedY;
if (x != 0d && y != 0d)
{
double angle = Math.Atan2(x, y) / (Math.PI * 2d);
if (angle < 0d) angle += 1d;
return CoerceValue(angle, 0d, 1d);
}
return 0d;
}
private double CalculateDeadZoneAndSaturation(double value, double deadZone, double saturation)
{
deadZone = CoerceValue(deadZone, 0.0d, 1.0d);
saturation = CoerceValue(saturation, 0.0d, 1.0d);
if ((deadZone > 0) | (saturation < 1))
{
double distance = CoerceValue(Math.Sqrt((X * X) + (Y * Y)), 0.0d, 1.0d);
double directionalDeadZone = Math.Abs(deadZone * (value / distance));
double directionalSaturation = 1 - Math.Abs((1 - saturation) * (value / distance));
double edgeSpace = (1 - directionalSaturation) + directionalDeadZone;
double multiplier = 1 / (1 - edgeSpace);
if (multiplier != 0)
{
if (value > 0)
{
value = (value - directionalDeadZone) * multiplier;
value = CoerceValue(value, 0, 1);
}
else
{
value = -((Math.Abs(value) - directionalDeadZone) * multiplier);
value = CoerceValue(value, -1, 0);
}
}
else
{
if (value > 0)
value = CoerceValue(value, directionalDeadZone, directionalSaturation);
else
value = CoerceValue(value, -directionalSaturation, -directionalDeadZone);
}
value = CoerceValue(value, -1, 1);
}
return value;
}
private double CalculateSensitivity(double value, double sensitivity)
{
value = CoerceValue(value, -1d, 1d);
if (sensitivity != 0)
{
double axisLevel = value;
axisLevel = axisLevel + ((axisLevel - Math.Sin(axisLevel * (Math.PI / 2))) * (sensitivity * 2));
if ((value < 0) & (axisLevel > 0))
axisLevel = 0;
if ((value > 0) & (axisLevel < 0))
axisLevel = 0;
value = CoerceValue(axisLevel, -1d, 1d);
}
return value;
}
private double CalculateRange(double value, double range)
{
value = CoerceValue(value, -1.0d, 1.0d);
range = CoerceValue(range, 0.0d, 1.0d);
if (range < 1)
{
double distance = CoerceValue(Math.Sqrt((X * X) + (Y * Y)), 0d, 1d);
double directionalRange = 1 - Math.Abs((1 - range) * (value / distance));
value *= CoerceValue(directionalRange, 0d, 1d);
}
return value;
}
}
I tried to make this question as short as possible, but it's hard for me to explain this specific problem without describing some details about it. I know I should keep it short, but I would like to write at least a few more words:
Thank you for having the time to read all this!