C# - 操纵杆灵敏度公式
C# - Joystick sensitivity formula
如何计算操纵杆灵敏度,同时考虑死区和操纵杆的圆形特性?
我正在研究 class 代表游戏手柄的一根棍子。我在数学方面遇到了麻烦,特别是灵敏度部分。灵敏度应该使操纵杆与中心的距离非线性。我在 X-Box 触发器上应用灵敏度没有问题,但是因为操纵杆有两个轴(X 和 Y),我在涉及的数学方面遇到了麻烦。
我想对摇杆应用圆形灵敏度,但我真的不知道该怎么做,特别是考虑到轴上的其他计算(如死区、距中心的距离等)。我该如何做到这一点?
有关问题的其他详细信息
现在,我已经有了我的临时修复程序,但效果不是很好。当操纵杆方向是水平或垂直时它似乎工作,但当我将它移动到对角线方向时,似乎有问题。我的 Joystick
class 有一个 Distance
属性,它检索杆与中心的距离(从 0 到 1 的值)。我的 Distance
属性 运行良好,但是当我应用灵敏度时,如果我移动我的 josytick,在对角线方向上检索到的距离小于 1,而无论方向如何,它应该恰好为 1 .
下面,我包含了我的 Joystick
class 的简化版本,其中我删除了大部分不相关的代码。计算出的轴的 X 和 Y 位置由 ComputedX
和 ComputedY
属性检索。每个属性都应包括其轴最终位置(从 -1 到 1),同时考虑所有修饰符(死区、饱和度、灵敏度等)。
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;
}
}
我试图让这个问题尽可能简短,但是如果不描述它的一些细节,我很难解释这个具体问题。我知道我应该保持简短,但我想至少多写几个字:
感谢您有时间阅读所有这些内容!
在 Internet 上搜索了一些几何数学之后,我终于找到了解决问题的方法。我数学很差,现在才知道其实很简单
我应该将它们应用到操纵杆半径,而不是单独为每个轴应用死区和灵敏度。因此,为此,我只需要将操纵杆的笛卡尔坐标(X 和 Y)转换为极坐标(半径和角度)。然后,我在半径坐标上应用死区灵敏度和我想要的所有修改器,并将其转换回笛卡尔坐标。
我在这里发布我正在使用的代码。这看起来比我上面问题中的代码更简单、更清晰:
private void ComputeCoordinates()
{
// Convert to polar coordinates.
double r = CoerceValue(Math.Sqrt((X * X) + (Y * Y)), 0d, 1d); // Radius;
double a = Math.Atan2(Y, X); // Angle (in radians);
// Apply modifiers.
double value = ComputeModifiers(r);
// Convert to cartesian coordinates.
double x = value * Math.Cos(a);
double y = value * Math.Sin(a);
// Apply axis independent modifiers.
if (InvertX) x = -x;
if (InvertY) y = -y;
// Set calculated values to property values;
_computedX = x;
_computedY = y;
}
private double ComputeModifiers(double value)
{
// Apply dead-zone and saturation.
if (DeadZone > 0d || Saturation < 1d)
{
double edgeSpace = (1 - Saturation) + DeadZone;
if (edgeSpace < 1d)
{
double multiplier = 1 / (1 - edgeSpace);
value = (value - DeadZone) * multiplier;
value = CoerceValue(value, 0d, 1d);
}
else
{
value = Math.Round(value);
}
}
// Apply sensitivity.
if (Sensitivity != 0d)
{
value = value + ((value - Math.Sin(value * (Math.PI / 2))) * (Sensitivity * 2));
value = CoerceValue(value, 0d, 1d);
}
// Apply range.
if (Range < 1d)
{
value = value * Range;
}
// Return calculated value.
return CoerceValue(value, 0d, 1d);
}
上面代码的解释
- 将物理摇杆的X和Y坐标转换为极坐标;
- 将死区、饱和度、灵敏度和范围修改器应用于半径坐标;
- 使用原始角度和修改后的半径转换回笛卡尔坐标(X 和 Y);
- 可选:将独立于轴的修饰符应用于每个新轴(在这种情况下,如果用户希望轴反转,我只是反转每个轴);
- 完成。现在,无论我向哪个方向移动操纵杆,每个修改器都以循环方式应用;
嗯,这种情况让我花了大约一天的时间,因为我在互联网上没有找到任何与我的问题相关的东西,我也不太清楚如何搜索解决方案,但我希望其他人回答这个问题的人可能会觉得这很有用。
这里有一些关于笛卡尔坐标系和极坐标系的参考资料:
https://en.wikipedia.org/wiki/Cartesian_coordinate_system
以下对我来说效果很好。它采用标准抛物线 (x^2) 并确保结果已签名。您可以使用图形计算器调整曲线,使其更接近您的需求。
实际上,f(-1) = -1,f(0) = 0,f(1) = 1,中间的曲线不太敏感。
Mathf.Pow(axes.x, 2) * (axes.x < 0 ?-1 : 1)
如何计算操纵杆灵敏度,同时考虑死区和操纵杆的圆形特性?
我正在研究 class 代表游戏手柄的一根棍子。我在数学方面遇到了麻烦,特别是灵敏度部分。灵敏度应该使操纵杆与中心的距离非线性。我在 X-Box 触发器上应用灵敏度没有问题,但是因为操纵杆有两个轴(X 和 Y),我在涉及的数学方面遇到了麻烦。
我想对摇杆应用圆形灵敏度,但我真的不知道该怎么做,特别是考虑到轴上的其他计算(如死区、距中心的距离等)。我该如何做到这一点?
有关问题的其他详细信息
现在,我已经有了我的临时修复程序,但效果不是很好。当操纵杆方向是水平或垂直时它似乎工作,但当我将它移动到对角线方向时,似乎有问题。我的 Joystick
class 有一个 Distance
属性,它检索杆与中心的距离(从 0 到 1 的值)。我的 Distance
属性 运行良好,但是当我应用灵敏度时,如果我移动我的 josytick,在对角线方向上检索到的距离小于 1,而无论方向如何,它应该恰好为 1 .
下面,我包含了我的 Joystick
class 的简化版本,其中我删除了大部分不相关的代码。计算出的轴的 X 和 Y 位置由 ComputedX
和 ComputedY
属性检索。每个属性都应包括其轴最终位置(从 -1 到 1),同时考虑所有修饰符(死区、饱和度、灵敏度等)。
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;
}
}
我试图让这个问题尽可能简短,但是如果不描述它的一些细节,我很难解释这个具体问题。我知道我应该保持简短,但我想至少多写几个字:
感谢您有时间阅读所有这些内容!
在 Internet 上搜索了一些几何数学之后,我终于找到了解决问题的方法。我数学很差,现在才知道其实很简单
我应该将它们应用到操纵杆半径,而不是单独为每个轴应用死区和灵敏度。因此,为此,我只需要将操纵杆的笛卡尔坐标(X 和 Y)转换为极坐标(半径和角度)。然后,我在半径坐标上应用死区灵敏度和我想要的所有修改器,并将其转换回笛卡尔坐标。
我在这里发布我正在使用的代码。这看起来比我上面问题中的代码更简单、更清晰:
private void ComputeCoordinates()
{
// Convert to polar coordinates.
double r = CoerceValue(Math.Sqrt((X * X) + (Y * Y)), 0d, 1d); // Radius;
double a = Math.Atan2(Y, X); // Angle (in radians);
// Apply modifiers.
double value = ComputeModifiers(r);
// Convert to cartesian coordinates.
double x = value * Math.Cos(a);
double y = value * Math.Sin(a);
// Apply axis independent modifiers.
if (InvertX) x = -x;
if (InvertY) y = -y;
// Set calculated values to property values;
_computedX = x;
_computedY = y;
}
private double ComputeModifiers(double value)
{
// Apply dead-zone and saturation.
if (DeadZone > 0d || Saturation < 1d)
{
double edgeSpace = (1 - Saturation) + DeadZone;
if (edgeSpace < 1d)
{
double multiplier = 1 / (1 - edgeSpace);
value = (value - DeadZone) * multiplier;
value = CoerceValue(value, 0d, 1d);
}
else
{
value = Math.Round(value);
}
}
// Apply sensitivity.
if (Sensitivity != 0d)
{
value = value + ((value - Math.Sin(value * (Math.PI / 2))) * (Sensitivity * 2));
value = CoerceValue(value, 0d, 1d);
}
// Apply range.
if (Range < 1d)
{
value = value * Range;
}
// Return calculated value.
return CoerceValue(value, 0d, 1d);
}
上面代码的解释
- 将物理摇杆的X和Y坐标转换为极坐标;
- 将死区、饱和度、灵敏度和范围修改器应用于半径坐标;
- 使用原始角度和修改后的半径转换回笛卡尔坐标(X 和 Y);
- 可选:将独立于轴的修饰符应用于每个新轴(在这种情况下,如果用户希望轴反转,我只是反转每个轴);
- 完成。现在,无论我向哪个方向移动操纵杆,每个修改器都以循环方式应用;
嗯,这种情况让我花了大约一天的时间,因为我在互联网上没有找到任何与我的问题相关的东西,我也不太清楚如何搜索解决方案,但我希望其他人回答这个问题的人可能会觉得这很有用。
这里有一些关于笛卡尔坐标系和极坐标系的参考资料:
https://en.wikipedia.org/wiki/Cartesian_coordinate_system
以下对我来说效果很好。它采用标准抛物线 (x^2) 并确保结果已签名。您可以使用图形计算器调整曲线,使其更接近您的需求。
实际上,f(-1) = -1,f(0) = 0,f(1) = 1,中间的曲线不太敏感。
Mathf.Pow(axes.x, 2) * (axes.x < 0 ?-1 : 1)