操纵杆死区计算
我的问题:给定x和y,我需要计算所需操纵杆偏转的x和y。操纵杆死区计算
当没有操纵杆死区时,这很简单 - 我只是在没有操纵的情况下使用x和y。
当存在死区时,我希望x = 0为零,并且x =非零是该方向上死区之外的第一个值。
方形盲区很简单。在下面的代码中,x和y从-1到1。死区范围从0到1。
float xDeflection = 0;
if (x > 0)
xDeflection = (1 - deadzone) * x + deadzone;
else if (x < 0)
xDeflection = (1 - deadzone) * x - deadzone;
float yDeflection = 0;
if (y > 0)
yDeflection = (1 - deadzone) * y + deadzone;
else if (y < 0)
yDeflection = (1 - deadzone) * y - deadzone;
圆形死区更棘手。一大堆鬼混后,我想出了这个:
float xDeflection = 0, yDeflection = 0;
if (x != 0 || y != 0) {
float distRange = 1 - deadzone;
float dist = distRange * (float)Math.sqrt(x * x + y * y) + deadzone;
double angle = Math.atan2(x, y);
xDeflection = dist * (float)Math.sin(angle);
yDeflection = dist * (float)Math.cos(angle);
}
以下是此输出在极端操纵杆偏转(盲区= 0.25):
Non-square joystick deflection. http://n4te.com/temp/nonsquare.gif
,你可以看,偏转不会延伸到角落。 IE,如果x = 1,y = 1那么xDeflection和yDeflection都等于0.918。问题会随着更大的死区而恶化,使得上图中的绿色线条越来越像一个圆圈。在死区= 1时,绿线是一个与死区相匹配的圆。
我发现,用一个小的变化,我可以放大由-1到1外部的绿线和夹值表示的形状:
if (x != 0 || y != 0) {
float distRange = 1 - 0.71f * deadzone;
float dist = distRange * (float)Math.sqrt(x * x + y * y) + deadzone;
double angle = Math.atan2(x, y);
xDeflection = dist * (float)Math.sin(angle);
xDeflection = Math.min(1, Math.max(-1, xDeflection));
yDeflection = dist * (float)Math.cos(angle);
yDeflection = Math.min(1, Math.max(-1, yDeflection));
}
我从试验和错误想出了恒定0.71。这个数字使得形状足够大,以至于角落在实际角落的小数点后面。出于学术原因,任何人都可以解释为什么0.71恰好是这样的数字?总之,我不确定我是否采取了正确的方法。有没有更好的方法来完成我需要的循环盲区?
是怎么回事我写了一个简单的基于Swing的程序可视化:
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Hashtable;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class DeadzoneTest extends JFrame {
float xState, yState;
float deadzone = 0.3f;
int size = (int)(255 * deadzone);
public DeadzoneTest() {
super("DeadzoneTest");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
final CardLayout cardLayout = new CardLayout();
final JPanel centerPanel = new JPanel(cardLayout);
getContentPane().add(centerPanel, BorderLayout.CENTER);
centerPanel.setPreferredSize(new Dimension(512, 512));
Hashtable labels = new Hashtable();
labels.put(-255, new JLabel("-1"));
labels.put(-128, new JLabel("-0.5"));
labels.put(0, new JLabel("0"));
labels.put(128, new JLabel("0.5"));
labels.put(255, new JLabel("1"));
final JSlider ySlider = new JSlider(JSlider.VERTICAL, -256, 256, 0);
getContentPane().add(ySlider, BorderLayout.EAST);
ySlider.setInverted(true);
ySlider.setLabelTable(labels);
ySlider.setPaintLabels(true);
ySlider.setMajorTickSpacing(32);
ySlider.setSnapToTicks(true);
ySlider.addChangeListener(new ChangeListener() {
public void stateChanged (ChangeEvent event) {
yState = ySlider.getValue()/255f;
centerPanel.repaint();
}
});
final JSlider xSlider = new JSlider(JSlider.HORIZONTAL, -256, 256, 0);
getContentPane().add(xSlider, BorderLayout.SOUTH);
xSlider.setLabelTable(labels);
xSlider.setPaintLabels(true);
xSlider.setMajorTickSpacing(32);
xSlider.setSnapToTicks(true);
xSlider.addChangeListener(new ChangeListener() {
public void stateChanged (ChangeEvent event) {
xState = xSlider.getValue()/255f;
centerPanel.repaint();
}
});
final JSlider deadzoneSlider = new JSlider(JSlider.VERTICAL, 0, 100, 33);
getContentPane().add(deadzoneSlider, BorderLayout.WEST);
deadzoneSlider.setInverted(true);
deadzoneSlider.createStandardLabels(25);
deadzoneSlider.setPaintLabels(true);
deadzoneSlider.setMajorTickSpacing(25);
deadzoneSlider.setSnapToTicks(true);
deadzoneSlider.addChangeListener(new ChangeListener() {
public void stateChanged (ChangeEvent event) {
deadzone = deadzoneSlider.getValue()/100f;
size = (int)(255 * deadzone);
centerPanel.repaint();
}
});
final JComboBox combo = new JComboBox();
combo.setModel(new DefaultComboBoxModel(new Object[] {"round", "square"}));
getContentPane().add(combo, BorderLayout.NORTH);
combo.addActionListener(new ActionListener() {
public void actionPerformed (ActionEvent event) {
cardLayout.show(centerPanel, (String)combo.getSelectedItem());
}
});
centerPanel.add(new Panel() {
public void toDeflection (Graphics g, float x, float y) {
g.drawRect(256 - size, 256 - size, size * 2, size * 2);
float xDeflection = 0;
if (x > 0)
xDeflection = (1 - deadzone) * x + deadzone;
else if (x < 0) {
xDeflection = (1 - deadzone) * x - deadzone;
}
float yDeflection = 0;
if (y > 0)
yDeflection = (1 - deadzone) * y + deadzone;
else if (y < 0) {
yDeflection = (1 - deadzone) * y - deadzone;
}
draw(g, xDeflection, yDeflection);
}
}, "square");
centerPanel.add(new Panel() {
public void toDeflection (Graphics g, float x, float y) {
g.drawOval(256 - size, 256 - size, size * 2, size * 2);
float xDeflection = 0, yDeflection = 0;
if (x != 0 || y != 0) {
float distRange = 1 - 0.71f * deadzone;
float dist = distRange * (float)Math.sqrt(x * x + y * y) + deadzone;
double angle = Math.atan2(x, y);
xDeflection = dist * (float)Math.sin(angle);
xDeflection = Math.min(1, Math.max(-1, xDeflection));
yDeflection = dist * (float)Math.cos(angle);
yDeflection = Math.min(1, Math.max(-1, yDeflection));
}
draw(g, xDeflection, yDeflection);
}
}, "round");
cardLayout.show(centerPanel, (String)combo.getSelectedItem());
pack();
setLocationRelativeTo(null);
setVisible(true);
}
private abstract class Panel extends JPanel {
public void paintComponent (Graphics g) {
g.setColor(Color.gray);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.white);
g.fillRect(0, 0, 512, 512);
g.setColor(Color.green);
if (true) {
// Draws all edge points.
for (int i = -255; i < 256; i++)
toDeflection(g, i/255f, 1);
for (int i = -255; i < 256; i++)
toDeflection(g, i/255f, -1);
for (int i = -255; i < 256; i++)
toDeflection(g, 1, i/255f);
for (int i = -255; i < 256; i++)
toDeflection(g, -1, i/255f);
} else if (false) {
// Draws all possible points (slow).
for (int x = -255; x < 256; x++)
for (int y = -255; y < 256; y++)
toDeflection(g, x/255f, y/255f);
}
g.setColor(Color.red);
toDeflection(g, xState, yState);
}
abstract public void toDeflection (Graphics g, float x, float y);
public void draw (Graphics g, float xDeflection, float yDeflection) {
int r = 5, d = r * 2;
g.fillRect((int)(xDeflection * 256) + 256 - r, (int)(yDeflection * 256) + 256 - r, d, d);
}
}
public static void main (String[] args) {
new DeadzoneTest();
}
}
这是我扔在一起。它的表现有点奇怪,但在边界很好:
private Point2D.Float calculateDeflection(float x, float y) {
Point2D.Float center = new Point2D.Float(0, 0);
Point2D.Float joyPoint = new Point2D.Float(x, y);
Double angleRad = Math.atan2(y, x);
float maxDist = getMaxDist(joyPoint);
float factor = (maxDist - deadzone)/maxDist;
Point2D.Float factoredPoint = new Point2D.Float(x * factor, y * factor);
float factoredDist = (float) center.distance(factoredPoint);
float finalDist = factoredDist + deadzone;
float finalX = finalDist * (float) Math.cos(angleRad);
float finalY = finalDist * (float) Math.sin(angleRad);
Point2D.Float finalPoint = new Point2D.Float(finalX, finalY);
return finalPoint;
}
编辑:错过了这一个。
private float getMaxDist(Point2D.Float point) {
float xMax;
float yMax;
if (Math.abs(point.x) > Math.abs(point.y)) {
xMax = Math.signum(point.x);
yMax = point.y * point.x/xMax;
} else {
yMax = Math.signum(point.y);
xMax = point.x * point.y/yMax;
}
Point2D.Float maxPoint = new Point2D.Float(xMax, yMax);
Point2D.Float center = new Point2D.Float(0, 0);
return (float) center.distance(maxPoint);
}
它保留了角度,但是将距离从0和边界之间的距离缩放到死区和边界之间。最大距离由于在侧面为1而在角落为sqrt(2)而变化,所以缩放必须相应地改变。
如果你有一个圆形的盲区实际上0.71为0.70710678或2 由于计算的平方根的一半到毕达哥拉斯定理
啊哈!很高兴知道0.71不是魔术。 :)一张照片会太棒了! – NateS 2009-12-22 09:38:57
我想尝试解决问题有点不同。正如我理解你的要求,算法应
- 回报的x/y值,如果操纵杆位置是死区外
- 回0/Y,X/0或0/0,如果操纵杆(部分地)在死区内
假设操纵杆被向上推,但x在定义的水平死区内,则需要坐标(0,y)作为结果。
因此,在第一步中,我会测试游戏杆坐标是否在定义的死区内。对于一个圆来说,它非常简单,只需将x/y坐标转换为距离(Pythagoras),然后检查该距离是否小于圆半径。
如果在外面,则返回(x/y)。如果它在里面,检查x,如果这些值在水平或垂直死区内。
这里有一个草案,勾勒出我的想法:
private Point convertRawJoystickCoordinates(int x, int y, double deadzoneRadius) {
Point result = new Point(x,y); // a class with just two members, int x and int y
boolean isInDeadzone = testIfRawCoordinatesAreInDeadzone(x,y,radius);
if (isInDeadzone) {
result.setX(0);
result.setY(0);
} else {
if (Math.abs((double) x) < deadzoneRadius) {
result.setX(0);
}
if (Math.abs((double) y) < deadzoneRadius) {
result.setY(0);
}
}
return result;
}
private testIfRawCoordinatesAreInDeadzone(int x, int y, double radius) {
double distance = Math.sqrt((double)(x*x)+(double)(y*y));
return distance < radius;
}
编辑
上述想法使用原始坐标,所以假设原始x值范围为[-255,255],半径为2并将游戏杆设置为x值(-3,-2,-1,0,1,2,3),则会产生序列(-3,0,0,0,0,0,3)。所以死区是空白的,但是从0跳到3。如果这是不需要的,我们可以将非死区从([-256,-radius],[radius,256])'拉伸'到(标准化的)范围([-1,0],[0,1])。
所以我只需要归一化转换的原始点:
private Point normalize(Point p, double radius) {
double validRangeX = MAX_X - radius;
double validRangeY = MAX_Y - radius;
double x = (double) p.getX();
double y = (double) p.getY();
return new Point((x-r)/validXRange, (y-r)/validYRange);
}
简言之:它规范化为x轴和y轴的有效范围(范围减去死区半径)为[-1,1] ,以便raw_x = radius被转换为normalized_x = 0。
(该方法应该适用于正负值至少我希望是这样,我手头没有IDE或JDK目前测试;))
感谢您的详细解答Andreas_D。不幸的是,它不符合我的要求。一点背景可能会有所帮助。 [我的项目](http://code.google.com/p/pg3b/)使用PC来操纵Xbox控制器。这使问题有点独特,因为通常游戏杆是只读的。给定从-1到1的x值,我想将游戏杆从-1偏移到1.棘手的部分是我想如何忽略死区。例如,如果x = 0.5,那么使用(1-deadzone)* x + deadzone,得到xDeflection = 0.6,方形死区为0.2。 – NateS 2009-12-22 09:06:46
上次编辑后更好吗? - 啊,上面的想法使用原始游戏杆坐标中定义的死区,也许这是令人困惑的。我尽可能使用原始值。 – 2009-12-22 09:53:48
不,对不起。我需要与你正在做的事情相反 - 我需要从规范化的价值转向原始价值。 – NateS 2009-12-22 11:33:32
+1有趣的问题和精湛的演示文稿。示例代码非常好地可视化您的问题。 – Buhb 2009-12-22 06:59:08
谢谢! :) 如果您对我为什么试图解决这个问题感到好奇(操纵杆通常是只读的!),它适用于PG3B项目: http://code.google.com/p/pg3b/ – NateS 2009-12-22 08:10:47