2009-12-22 145 views
4

我的問題:給定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(); 
} 
} 
+0

+1有趣的問題和精湛的演示文稿。示例代碼非常好地可視化您的問題。 – Buhb 2009-12-22 06:59:08

+0

謝謝! :) 如果您對我爲什麼試圖解決這個問題感到好奇(操縱桿通常是隻讀的!),它適用於PG3B項目: http://code.google.com/p/pg3b/ – NateS 2009-12-22 08:10:47

回答

3

這是我扔在一起。它的表現有點奇怪,但在邊界很好:

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

感謝Buhb。我想嘗試一下,但getMaxDist的定義是什麼? – NateS 2009-12-22 10:59:21

+0

絕對太棒了!太棒了。我還沒有完全理解它,但我會研究它。謝謝Buhb! – NateS 2009-12-22 11:30:03

+0

這一直很好。我發現一個小的改進來計算maxDist沒有sqrt:1/cos * Math.signum(x)和1/sin * Math.signum(y) – NateS 2010-01-07 01:22:25

4

如果你有一個圓形的盲區實際上0.71爲0.70710678或2 由於計算的平方根的一半到畢達哥拉斯定理

+0

啊哈!很高興知道0.71不是魔術。 :)一張照片會太棒了! – NateS 2009-12-22 09:38:57

3

我想嘗試解決問題有點不同。正如我理解你的要求,算法應

  1. 回報的x/y值,如果操縱桿位置是死區外
  2. 回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目前​​測試;))

+0

感謝您的詳細解答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

+0

上次編輯後更好嗎? - 啊,上面的想法使用原始遊戲杆座標中定義的死區,也許這是令人困惑的。我儘可能使用原始值。 – 2009-12-22 09:53:48

+0

不,對不起。我需要與你正在做的事情相反 - 我需要從規範化的價值轉向原始價值。 – NateS 2009-12-22 11:33:32