Creating Engine Support for Android Sensor Input

BTW, if you have a real joystick, you should be able to plug it in and see if the app works the same when using the PC version with a real joystick plugged in.

unfortunately i don’t have a real joystick, might need to invest in 1 :stuck_out_tongue:



also I tried with the deadzone of 0.05 and that made the cube snap to the center, when it got near it. Which now that i think about it was what the jitter I had before was doing

Android Orientation Joystick has been updated to support the new Joystick modifications. When I get a minute, I’ll update this post with a new example of how to use the joystick interface to get the orientation feedback.

1 Like

Here is the test I’ve been using for Android sensors. It’s a bit big, but it shows how to use the Android Orientation data to influence the FlyByCamera or control the orientation of an object in the scene. It also allows you to set the rumble of the joystick to vibrate the device (if the device has a vibrate capability).



Comment / Uncomment the first few lines of simpleInitApp to setup what you want the sensor data to do.



It uses a texture from test-data (Interface/Logo/Monkey.jpg) so if you’re not including that library, grab the file and place it in you assets directory.



Enjoy.



[java]

package mygame;



import com.jme3.app.SimpleApplication;

import com.jme3.input.Joystick;

import com.jme3.input.JoystickAxis;

import com.jme3.input.MouseInput;

import com.jme3.input.SensorJoystickAxis;

import com.jme3.input.controls.ActionListener;

import com.jme3.input.controls.AnalogListener;

import com.jme3.input.controls.MouseButtonTrigger;

import com.jme3.material.Material;

import com.jme3.math.ColorRGBA;

import com.jme3.math.FastMath;

import com.jme3.math.Quaternion;

import com.jme3.math.Vector3f;

import com.jme3.scene.Geometry;

import com.jme3.scene.Mesh;

import com.jme3.scene.shape.Box;

import com.jme3.scene.shape.Line;

import com.jme3.system.AppSettings;

import com.jme3.texture.Texture;

import com.jme3.util.IntMap;

import com.jme3.util.IntMap.Entry;

import java.util.List;

import java.util.logging.Level;

import java.util.logging.Logger;



public class Main extends SimpleApplication implements ActionListener, AnalogListener {

private static final Logger logger = Logger.getLogger(Main.class.getName());

private Geometry geomZero = null;



// Map of joysticks saved with the joyId as the key

private IntMap<Joystick> joystickMap = new IntMap<Joystick>();



// flag to allow for the joystick axis to be calibrated on startup

private boolean initialCalibrationComplete = false;



// mappings used for onAnalog

private final String ORIENTATION_X_PLUS = “Orientation_X_Plus”;

private final String ORIENTATION_X_MINUS = “Orientation_X_Minus”;

private final String ORIENTATION_Y_PLUS = “Orientation_Y_Plus”;

private final String ORIENTATION_Y_MINUS = “Orientation_Y_Minus”;

private final String ORIENTATION_Z_PLUS = “Orientation_Z_Plus”;

private final String ORIENTATION_Z_MINUS = “Orientation_Z_Minus”;



// variables to save the current rotation

// Used when controlling the geometry with device orientation

private float[] anglesCurrent = new float[] {0f, 0f, 0f};

private Quaternion rotationQuat = new Quaternion();



// switch to apply an absolute rotation (geometry.setLocalRotation) or

// an incremental constant rotation (geometry.rotate)

// Used when controlling the geometry with device orientation

private boolean useAbsolute = false;



// rotation speed to use when apply constant incremental rotation

// Used when controlling the geometry with device orientation

private float rotationSpeedX = 1f;

private float rotationSpeedY = 1f;



// current intensity of the rumble

float rumbleAmount = 0f;



// toggle to enable rumble

boolean enableRumble = false;



// toggle to enable device orientation in FlyByCamera

boolean enableFlyByCameraRotation = false;



// toggle to enable controlling geometry rotation

boolean enableGeometryRotation = false;



// Make sure to set joystickEventsEnabled = true in MainActivity for Android



public static void main(String[] args) {

Main app = new Main();

AppSettings settings = new AppSettings(true);

settings.setUseJoysticks(true);

app.setSettings(settings);

app.start();

}



private float toDegrees(float rad) {

return rad*FastMath.RAD_TO_DEG;

}



@Override

public void simpleInitApp() {

// setup configuration for this test program

// enableFlyByCameraRotation = true;

enableGeometryRotation = true;

// useAbsolute = true;

// enableRumble = true;





if (enableFlyByCameraRotation) {

// if setDragToRotate is set, then camera rotations are only applied

// if the mouse is being clicked (ie. finger pressing on the screen)

// flyCam.setDragToRotate(true);

flyCam.setEnabled(true);

} else {

flyCam.setEnabled(false);

}



Mesh lineX = new Line(Vector3f.ZERO, Vector3f.ZERO.add(Vector3f.UNIT_X.mult(3)));

Mesh lineY = new Line(Vector3f.ZERO, Vector3f.ZERO.add(Vector3f.UNIT_Y.mult(3)));

Mesh lineZ = new Line(Vector3f.ZERO, Vector3f.ZERO.add(Vector3f.UNIT_Z.mult(3)));



lineX.setLineWidth(2);

lineX.setLineWidth(2);

lineX.setLineWidth(2);



Geometry geoX = new Geometry(“X”, lineX);

Material matX = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

matX.setColor(“Color”, ColorRGBA.Red);

geoX.setMaterial(matX);

rootNode.attachChild(geoX);



Geometry geoY = new Geometry(“Y”, lineY);

Material matY = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

matY.setColor(“Color”, ColorRGBA.Green);

geoY.setMaterial(matY);

rootNode.attachChild(geoY);



Geometry geoZ = new Geometry(“Z”, lineZ);

Material matZ = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

matZ.setColor(“Color”, ColorRGBA.Blue);

geoZ.setMaterial(matZ);

rootNode.attachChild(geoZ);



Box b = new Box(Vector3f.ZERO, 1, 1, 1);

geomZero = new Geometry(“Box”, b);



Material mat = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

mat.setColor(“Color”, ColorRGBA.Yellow);

Texture tex_ml = assetManager.loadTexture(“Interface/Logo/Monkey.jpg”);

mat.setTexture(“ColorMap”, tex_ml);



geomZero.setMaterial(mat);

geomZero.setLocalTranslation(Vector3f.ZERO);

geomZero.setLocalRotation(Quaternion.IDENTITY);

rootNode.attachChild(geomZero);



// Touch (aka MouseInput.BUTTON_LEFT) is used to record the starting

// orientation when using absolute rotations

inputManager.addMapping(“MouseClick”, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));

inputManager.addListener(this, “MouseClick”);



Joystick[] joysticks = inputManager.getJoysticks();

if (joysticks == null || joysticks.length < 1) {

logger.log(Level.INFO, “Cannot find any joysticks!”);



} else {

// Joysticks return a value of 0 to 1 based on how far the stick is

// push on the axis. This value is then scaled based on how long

// during the frame the joystick axis has been in that position.

// If the joystick is push all the way for the whole frame,

// then the value in onAnalog is equal to tpf.

// If the joystick is push 1/2 way for the entire frame, then the

// onAnalog value is 1/2 tpf.

// Similarly, if the joystick is pushed to the maximum during a frame

// the value in onAnalog will also be scaled.

// For Android sensors, rotating the device 90deg is the same as

// pushing an actual joystick axis to the maximum.



logger.log(Level.INFO, “Number of joysticks: {0}”, joysticks.length);

JoystickAxis axis;

for (Joystick joystick: joysticks) {



// Get and display all axes in joystick.

List<JoystickAxis> axes = joystick.getAxes();

for (JoystickAxis joystickAxis: axes) {

logger.log(Level.INFO, “{0} axis scan Name: {1}, LogicalId: {2}, AxisId: {3}”,

new Object[]{joystick.getName(), joystickAxis.getName(), joystickAxis.getLogicalId(), joystickAxis.getAxisId()});

}



// Get specific axis based on LogicalId of the JoystickAxis

// If found, map axis

axis = joystick.getAxis(SensorJoystickAxis.ORIENTATION_X);

if (axis != null) {

axis.assignAxis(ORIENTATION_X_PLUS, ORIENTATION_X_MINUS);

inputManager.addListener(this, ORIENTATION_X_PLUS, ORIENTATION_X_MINUS);

logger.log(Level.INFO, “Found {0} Joystick, assigning mapping for X axis: {1}, with max value: {2}”,

new Object[]{joystick.toString(), axis.toString(), ((SensorJoystickAxis)axis).getMaxRawValue()});



}

axis = joystick.getAxis(SensorJoystickAxis.ORIENTATION_Y);

if (axis != null) {

axis.assignAxis(ORIENTATION_Y_PLUS, ORIENTATION_Y_MINUS);

inputManager.addListener(this, ORIENTATION_Y_PLUS, ORIENTATION_Y_MINUS);

logger.log(Level.INFO, “Found {0} Joystick, assigning mapping for Y axis: {1}, with max value: {2}”,

new Object[]{joystick.toString(), axis.toString(), ((SensorJoystickAxis)axis).getMaxRawValue()});



}

axis = joystick.getAxis(SensorJoystickAxis.ORIENTATION_Z);

if (axis != null) {

axis.assignAxis(ORIENTATION_Z_PLUS, ORIENTATION_Z_MINUS);

inputManager.addListener(this, ORIENTATION_Z_PLUS, ORIENTATION_Z_MINUS);

logger.log(Level.INFO, “Found {0} Joystick, assigning mapping for Z axis: {1}, with max value: {2}”,

new Object[]{joystick.toString(), axis.toString(), ((SensorJoystickAxis)axis).getMaxRawValue()});



}



joystickMap.put(joystick.getJoyId(), joystick);



}

}

}



@Override

public void simpleUpdate(float tpf) {

if (!initialCalibrationComplete) {

// Calibrate the axis (set new zero position) if the axis

// is a sensor joystick axis

for (Entry<Joystick> entry: joystickMap) {

for (JoystickAxis axis: entry.getValue().getAxes()) {

if (axis instanceof SensorJoystickAxis) {

logger.log(Level.INFO, “Calibrating Axis: {0}”, axis.toString());

((SensorJoystickAxis)axis).calibrateCenter();

}

}

}

initialCalibrationComplete = true;

}





if (enableGeometryRotation) {

rotationQuat.fromAngles(anglesCurrent);

rotationQuat.normalizeLocal();



if (useAbsolute) {

geomZero.setLocalRotation(rotationQuat);

} else {

geomZero.rotate(rotationQuat);

}

anglesCurrent[0] = anglesCurrent[1] = anglesCurrent[2] = 0f;

}



}



public void onAction(String string, boolean pressed, float tpf) {



if (string.equalsIgnoreCase(“MouseClick”) && pressed) {



// Calibrate the axis (set new zero position) if the axis

// is a sensor joystick axis

for (Entry<Joystick> entry: joystickMap) {

for (JoystickAxis axis: entry.getValue().getAxes()) {

if (axis instanceof SensorJoystickAxis) {

logger.log(Level.INFO, “Calibrating Axis: {0}”, axis.toString());

((SensorJoystickAxis)axis).calibrateCenter();

}

}

}



if (enableRumble) {

// manipulate joystick rumble

for (Entry<Joystick> entry: joystickMap) {

rumbleAmount += 0.1f;

if (rumbleAmount > 1f + FastMath.ZERO_TOLERANCE) {

rumbleAmount = 0f;

}

logger.log(Level.INFO, “rumbling with amount: {0}”, rumbleAmount);

entry.getValue().rumble(rumbleAmount);

}

}

}

}



public void onAnalog(String string, float value, float tpf) {

logger.log(Level.INFO, “onAnalog for {0}, value: {1}, tpf: {2}”,

new Object[]{string, value, tpf});



float scaledValue = value;



if (string.equalsIgnoreCase(ORIENTATION_X_PLUS)) {

if (useAbsolute) {

// set rotation amount

// divide by tpf to get back to actual axis value (0 to 1)

// multiply by 90deg so that 90deg = full axis (value = tpf)

anglesCurrent[0] = (scaledValue / tpf * FastMath.HALF_PI);

} else {

// apply an incremental rotation amount based on rotationSpeed

anglesCurrent[0] += scaledValue * rotationSpeedX;

}

}

if (string.equalsIgnoreCase(ORIENTATION_X_MINUS)) {

if (useAbsolute) {

// set rotation amount

// divide by tpf to get back to actual axis value (0 to 1)

// multiply by 90deg so that 90deg = full axis (value = tpf)

anglesCurrent[0] = (-scaledValue / tpf * FastMath.HALF_PI);

} else {

// apply an incremental rotation amount based on rotationSpeed

anglesCurrent[0] -= scaledValue * rotationSpeedX;

}

}

if (string.equalsIgnoreCase(ORIENTATION_Y_PLUS)) {

if (useAbsolute) {

// set rotation amount

// divide by tpf to get back to actual axis value (0 to 1)

// multiply by 90deg so that 90deg = full axis (value = tpf)

anglesCurrent[1] = (scaledValue / tpf * FastMath.HALF_PI);

} else {

// apply an incremental rotation amount based on rotationSpeed

anglesCurrent[1] += scaledValue * rotationSpeedY;

}

}

if (string.equalsIgnoreCase(ORIENTATION_Y_MINUS)) {

if (useAbsolute) {

// set rotation amount

// divide by tpf to get back to actual axis value (0 to 1)

// multiply by 90deg so that 90deg = full axis (value = tpf)

anglesCurrent[1] = (-scaledValue / tpf * FastMath.HALF_PI);

} else {

// apply an incremental rotation amount based on rotationSpeed

anglesCurrent[1] -= scaledValue * rotationSpeedY;

}

}



}



}



[/java]

2 Likes

can’t test it right now, but you are awesome!!!

Tested on Samsung Galaxy S2 using Gingerbread 2.3.3:



Here’s my test results (assuming orientation is based on landscape view) using original code setup 2 (maybe 3 by the time I submit this) posts above:


  1. Phone flat on desk → face-on view of cube and cube doesn’t rotate or jitters, perfect.
  2. Lift phone on +Y axis (tilt screen towards user) → cube rotates +Y direction
  3. Lift phone on +X axis (tilt screen using left hand up) → cube rotates +X direction
  4. No Z axis.
  5. Speed of rotation increases as tilt of axis increases. Y axis seems to appear to rotate faster than the X axis.
  6. Axis got messed up when tilted to 90 degree on the Y axis. This behavior was not seen on the X axis but once this happens, the axis are messed up. Restarting app resets sensor to initial condition.

    7, Cube reverses direction on X axis when screen is facing floor. Could be a big deal if expecting user to use sensors when they are lying down chilling. Again, once this happens, the axis are messed up. Restarting app resets sensor to initial condition.
  7. Tap screen stops rotation.



    Test results after setting the following:

    [java]// switch to apply an absolute rotation (geometry.setLocalRotation) or

    // an incremental constant rotation (geometry.rotate)

    // Used when controlling the geometry with device orientation

    private boolean useAbsolute = true;[/java]


  8. Phone flat on desk → face-on view of cube and cube doesn’t rotate or jitters, perfect.
  9. Lift phone on +Y axis (tilt screen towards user) → cube rotates +Y direction relative to device angle, perfect
  10. Lift phone on +X axis (tilt screen using left hand up) → cube rotates +X direction relative to device angle, perfect
  11. No Z axis.
  12. When approaching 90 degree Y axis, cube starts to rotate -X axis direction, getting worse as device gets closer to 90 degree Y axis (dead zone) to the point it jitters and rotates -X direction rapidly. If you continue beyond 90 degree on Y axis (screen is no longer facing user head-on but now starting to face the floor), cube no longer jitters and it rotates smoothly and properly.
  13. X axis does not have issue with 90 degree tilt, perfect.
  14. Tap screen sets current orientation position (cube face-on), perfect.



    Test results after setting the following:

    [java]// toggle to enable device orientation in FlyByCamera

    boolean enableFlyByCameraRotation = true;[/java]

    and

    [java]// toggle to enable rumble

    boolean enableRumble = true;[/java]


  15. Phone flat on desk → face-on view of cube and cube doesn’t rotate or jitters, perfect.
  16. Lift phone on +Y axis (tilt screen towards user) → cube rotates +Y direction relative to device angle, speed relative to device angle, cube move -Y direction (really this means the FlyByCamera is moving +Y or up, perfect
  17. Lift phone on +X axis (tilt screen using left hand up) → cube rotates +X direction relative to device angle, speed relative to device angle, cube move -X direction (really this means the FlyByCamera is moving +X or up, perfect
  18. Z axis is zoom by pinching, perfect. It took a bit to zoom out but when I did when trying to test 90 Y axis dead zone, I noticed that the cube enlarged and was stretched askew (fun fact: google’ing askew results in the search result window being askew to drive home the meaning) as it reached screen edges which implies that the view is parabolic.
  19. Tap screen sets current orientation position (cube face-on), perfect.
  20. Rumble throws Exception due to no declaration in the Manifest file so don’t forget to add the following:

    [java]<uses-permission android:name=“android.permission.VIBRATE” />[/java]

    Also, vibrator doesn’t stop vibrating when you tap screen, it continues.



    Does any one else concur with my results or see a different test case?



    Any way, it’s come a long way. Congrats on your recent work. I see you’ve done a lot. That damn dead-zone still haunts.



    I’ll be building on your work as I try to incorporate ROTATION_VECTOR sensor → http://developer.android.com/guide/topics/sensors/sensors_motion.html#sensors-motion-rotateI’d appreciate it if you could outline what files you modified / created to incorporate a new sensor for Android. Project scope would be great! I intend to control the FlyByCamera with ROTATION_VECTOR readings. I don’t intend, at least initially, to incorporate all platform support, just Android so I won’t commit anything to prevent breakage / feather-ruffling.



    Edit: Removed how to prevent screen from sleeping. I’m sleepy to give clear advise.



    Thanks in advance and I hope my results are helpful,

    Platz
3 Likes

@platz Thanks for the feedback.



The issue with the device being rotated almost 90 degrees is a pain. Android starts to rotate other axes at around 80deg and then swaps axes at the 90deg mark. I’m going to add a check to ignore the data around this area. The “laying in bed playing the game” will still be an issue though. Need to figure out when the axis swapping has occurred and adjust the data accordingly in order to solve that one.



The way my example used the vibrator, it increases the strength every tap of the screen until it reaches full strength and then turns off.



To put in the Rotation Vector sensor, it looks like a Quat instead of a Vector3, so you’ll have to decide if you want to include the w component. To include all 4 values, something like the changes below would be needed.



Create new static strings in SensorJoystickAxis. Something like:

[java]

public static String ROTATION_X = “Rotation_X”;

public static String ROTATION_Y = “Rotation_Y”;

public static String ROTATION_Z = “Rotation_Z”;

public static String ROTATION_W = “Rotation_Z”;



[/java]





In the loadJoysticks method of AndroidSensorJoyInput, something should go in like:

[java]

sensorData = initSensor(Sensor.TYPE_ROTATION_VECTOR);

if (sensorData != null) {

sensorData.lastValues = new float[4];

axis = joystick.addAxis(SensorJoystickAxis.ROTATION_X, SensorJoystickAxis.ROTATION_X, joystick.getAxisCount(), 1f);

sensorData.axes.add(axis);

axis = joystick.addAxis(SensorJoystickAxis.ROTATION_Y, SensorJoystickAxis.ROTATION_Y, joystick.getAxisCount(), 1f);

sensorData.axes.add(axis);

axis = joystick.addAxis(SensorJoystickAxis.ROTATION_Z, SensorJoystickAxis.ROTATION_Z, joystick.getAxisCount(), 1f);

sensorData.axes.add(axis);

axis = joystick.addAxis(SensorJoystickAxis.ROTATION_W, SensorJoystickAxis.ROTATION_W, joystick.getAxisCount(), 1f);

sensorData.axes.add(axis);

}

[/java]



The last parameter in joystick.addAxis (the “1f”) will need to be changed to something else. It defines what value to use as the joystick “full amount”. For orientation, I use 90deg. This way when the device is tilted 90deg, it simulates the joystick being pushed all the way. For the Rotation_Vector, you’ll need to decide what value from Android will represent the “full” joystick amount for each axis.



The existing onSensorChanged method should already automatically start adding the data to the eventQueue which will also be automatically sent to inputManager, so no changes should be needed there.



I was planning on testing to see if this would provide better results than the SensorManager.getOrientation method which is what is used today. getOrientation needs to be the backup because Rotation_Vector isn’t supported until Android 2.3 and we need to support 2.2 minimum. But if the results are better, then I might use it if it’s available and default down to getOrientation if it isn’t. We’ll see.

Just a couple of warnings about Rotation_Vector, the number of values returned from the sensor is hardware dependant. My phone is giving me back 3 floats instead of 4, so my hardware is not returning the w value.



Also, the Rotation_Vector is not relative to the device, so the numbers will change differently depending on how the user is holding the phone and which direction they are facing.



I’m not sure what you are going to use the Rotation_Vector for, but thought I would just let you know.

Hi there. I just tested your test case on a samsung galaxy ace ii(galaxy gt-i8160) and the app starts just fine, but when i rotate the phone, nothing happens, the box with the monkey logo doesn’t rotate. I’m using the lastest builds. And i’ve already added the “joystickEventsEnabled = true;” line in MainActivity.java.

Here’s the log file : http://www.2shared.com/file/axtdS0Mk/accelerometer-testing.html

cya

EDIT: the flag enableGeometryRotation is enabled but it still doesn’t work.

@glaucomardano, Sorry, didn’t see your post until just now.

The log looks like the joystick axes are being setup and mapped. Is the onAnalog not being called at all?

I noticed something in your log that I haven’t seen before:
[java]
I/WindowOrientationListener( 1708): orientation = 0 Tilt = 84 – 0 , 0 , 10
[/java]

What version of Android are you on?

Hi , the onanalog method is called only when i touch the screen, the rotate event is not fired at all. My phone is android 2.3.6.