Joystick Dead Zone Calculation

My problem: given x and y, I need to calculate x and y for the required deviation of the joystick.

It is simple when there is no dead zone of the joystick - I just use x and y without manipulation.

When there is a dead zone, I want x = 0 to be zero, and x = a non-zero value is the first value in this direction, which is outside the dead zone.

The square dead zone is simple. In the following code, x and y are from -1 to 1 inclusive. Dead zone from 0 to 1 inclusive.

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; 

The circular dead zone is more complicated. After a number of mistakes, I came up with this:

 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); } 

Here's what happens to deflect the joystick at extreme values ​​(deadzone = 0.25):

Deviation from the square deviation of the joystick. http://n4te.com/temp/nonsquare.gif

As you can see, the deviation does not apply to corners. IE, if x = 1, y = 1, then xDeflection and yDeflection are approximately 0.918. The problem worsens with large dead zones, making the green lines in the image above look more and more like a circle. In the dead zone = 1 green lines is the circle that corresponds to the dead zone.

I found that with a little change, I could increase the shape represented by the green lines and the clip values ​​outside -1 to 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)); } 

I came up with the constant 0.71 from trial and error. This number makes the shape large enough so that the angles are within a few decimal places from the actual angles. For academic reasons, can anyone explain why 0.71 turns out to be the number it does?

Overall, I'm not quite sure if I am finding the right approach. Is there a better way to accomplish what I need for a circular dead zone?

I wrote a simple Swing-based program for visual display:

 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(); } } 
+4
source share
3 answers

This is what I threw together. This behaves a little strange, but on the border it's good:

 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; } 

Edit: skipped this one.

 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); } 

It preserves the angle, but scales the distance from somewhere between 0 and the border between the dead zone and the border. The maximum distance changes, since it is 1 on the sides and sqrt (2) in the corners, so the scaling should be changed accordingly.

+3
source

If you have a ring dead zone, then .71 is actually 0.70710678 or half the square of 2 Calculation by Pythagorean theorem

+4
source

I would try to solve the problem a little differently. Since I understood your requirements, the algorithm should

  • return x / y if the joystick position is outside the dead zone.
  • return 0 / y, x / 0 or 0/0 if the joystick is (partially) inside the dead zone

Say that the joystick is pushed up, but x is in a certain horizontal dead zone, as a result you will get the coordinate (0, y).

So, in the first step, I would check if the coordinates of the joystick are inside a specific dead zone. For a circle, this is pretty simple, you just need to convert the x / y coordinates to a distance ( Pythagoras ) and check if this distance is less than the radius of the circle.

If it is outside, return (x / y). If it is inside, check x and if values ​​are in their horizontal or vertical dead zone.

Here is a project to outline my idea:

 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; } 

Edit

The above idea uses the original coordinates, so suppose the range of x is not [-255,255], the radius is 2, and you set the joystick to x (-3, -2, -1,0,1, 2,3 ), it will produce the sequence (-3,0,0,0,0,0,0,3). Thus, the dead zone is empty, but there is a jump from 0 to 3. If this is undesirable, we can “stretch” the non-dead zone from ([-256, -radius], [radius, 256]) to the (normalized) range ([ -1.0], [0.1]).

So I just need to normalize the converted raw points:

 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((xr)/validXRange, (yr)/validYRange); } 

In short: it normalizes the allowable ranges (range minus the deadband radius) for the x and y axis to [-1.1], so raw_x = radius is converted to normalized_x = 0.

(The method should work on positive and negative values. At least I hope I don't have an IDE or JDK at hand to check;))

+3
source

All Articles