views:

354

answers:

3

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

This is simple when there is no joystick deadzone -- I just use the x and y with no manipulation.

When there is a deadzone, I want x=0 to be zero and x=non-zero to be the first value in that direction that is outside the deadzone.

A square deadzone is simple. In the following code x and y are from -1 to 1 inclusive. The deadzone is 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;

A circular deadzone is trickier. After a whole lot of fooling around 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 is what this outputs for the joystick deflection at the extremes (deadzone=0.25):

Non-square joystick deflection.

As you can see, the deflection does not extend to the corners. IE, if x=1,y=1 then the xDeflection and yDeflection both equal something like 0.918. The problem worsens with larger deadzones, making the green lines in the image above look more and more like a circle. At deadzone=1 the green lines are a circle that matches the deadzone.

I found that with a small change I could enlarge the shape represented by the green lines and clip values outside of -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 that the corners are within a few decimal places of the actual corners. For academic reasons, can anyone explain why 0.71 happens to be the number that does this?

Overall, I'm not really sure if I am taking the right approach. Is there a better way to accomplish what I need for a circular deadzone?

I have written a simple Swing-based program to visual what is going on:

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  A: 

If you have a circular deadzone the .71 is actually 0.70710678 or the half of the squareroot of 2 Calculation thanks to theorem of Pythagoras

Peter
Aha! It is good to know 0.71 is not magic. :) A picture would be fantastic!
NateS
+3  A: 

I'd try to tackle the problem a bit differently. As I've understood your requirements, the algorithm should

  1. return the x/y values, if the joystick position is outside the deadzone
  2. return 0/y, x/0 or 0/0 if the joystick is (partially) inside the deadzone

Say the joystick is pushed up but x is inside the defined horizontal deadzone, you want the coordinate (0,y) as a result.

So in a first step, I'd test if the joystick coordinates are inside the defined deadzone. For a circle it's pretty easy, you just have to convert the x/y coordinates into a distance (Pythagoras) and check if this distance is less then the circles radius.

If it's outside, return (x/y). If it is inside, check for x and if the values are inside their horizontal or vertical deadzone.

Here's a draft 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 raw coordinates, so assume the raw x value range is [-255,255], the radius is 2 and you set the joystick to the x values (-3,-2,-1,0,1,2,3), it will produce the sequence (-3,0,0,0,0,0,3). So the deadzone is blanked, but there's a jump from 0 to 3. If that is unwanted, we can 'stretch' the non-deadzone 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((x-r)/validXRange, (y-r)/validYRange);
}

In brief: it normalizes the valid ranges (range minus deadzone radius) for x- and y-axis to [-1,1], so that raw_x=radius is converted to normalized_x=0.

(the method should work for positive and negative values. At least I hope it does, I have no IDE or JDK at hand at the moment to test ;) )

Andreas_D
Thanks for the detailed answer Andreas_D. Unfortunately it doesn't meet my requirements. A little background might help. [My project](http://code.google.com/p/pg3b/) uses the PC to manipulate an Xbox controller. This makes the problem a bit unique, since normally joysticks are readonly. Given an x value from -1 to 1, I want to set the joystick to be deflected from -1 to 1. The tricky part is in how I want to ignore the deadzone. Eg, with a square deadzone of 0.2, if x=0.5 then using (1-deadzone)*x+deadzone I get xDeflection=0.6.
NateS
Better after with the last edit? - ah, the above idea uses a deadzone defined in raw joystick coordinates, maybe that's confusing. I use raw values as long as possible.
Andreas_D
Nope, sorry. I need the opposite of what you are doing -- I need to go from the normalized value to the raw value.
NateS
At the end it's a good example that one should clarify on the requirements before starting to implement ;)
Andreas_D
+3  A: 

This is what I threw together. It behaves a bit wierd, but in the boundaries 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: missed 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 boundary to between deadzone and boundary. The maximum distance varies since it's 1 on the sides and sqrt(2) in the corners, so scaling must be altered accordingly.

Buhb
Thanks Buhb. I'd like to try it, but what is the definition of getMaxDist?
NateS
Absolutely fantastic! This is perfect. I don't completely understand it yet, but I will study it. Thanks Buhb!
NateS
This has been working great. I found one minor improvement to calculate maxDist without sqrt: 1 / cos * Math.signum(x) and 1 / sin * Math.signum(y)
NateS