Hi everyone,
I have spent the last couple of days trying to track down a fiendish bug in my game, and while I think I’ve found the cause, I would like to understand it more deeply.
Occasionally, for no apparent reason, my TestPlayers would spontaneously go from looking like this:
to collapsing into a heap:
When I debugged the local translations and bounds, everything looked fine. I looked at threading, local and world transforms, code interfering with the refreshFlags, added breakpoints and logging everywhere…
Eventually, I noticed that a rotation packet was being received that set the player node’s local rotation to the quaternion (0,0,0,0.5f), and it turns out that this seems to cause JME to ignore the local translation of children of the player node.
My question is: why does that happen? (and is it an invalid quaternion?)
Many thanks,
Duncan.
P.S. I have a SSCE that replicates the problem - press ‘B’ to toggle the bug:
package com.codealchemists.jme.test;
import com.jme3.app.SimpleApplication;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;
// Shows geometry local translations 'collapsing' when local rotation of parent node is set to (0,0,0,0.5f).
// Press 'B' to toggle the bug.
public class TestGeometryCollapseBug extends SimpleApplication {
private Node node;
private boolean usingBuggyQuaternion = false;
public static void main(String[] args) {
TestGeometryCollapseBug app = new TestGeometryCollapseBug();
app.setShowSettings(false); // disable the initial settings dialog window
AppSettings appSettings =new AppSettings(true);
appSettings.setWidth(1024);
appSettings.setHeight(768);
app.setSettings(appSettings);
app.start();
}
@Override
public void simpleInitApp() {
addDirectionalLight();
addAmbientLight();
Material mat = createLightingMaterial(ColorRGBA.Red);
node = createPlayerEntityNode(mat);
rootNode.attachChild(node);
flyCam.setMoveSpeed(30);
inputManager.addListener(new ActionListener() {
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if (isPressed) {
if (name.equals("toggle")) {
usingBuggyQuaternion = !usingBuggyQuaternion;
if( usingBuggyQuaternion ) {
node.setLocalRotation(new Quaternion(0,0,0,0.5f));
} else {
node.setLocalRotation(new Quaternion(0,0,0,1.0f));
}
System.out.println("Set local rotation to "+node.getLocalRotation());
}
}
}
}, "toggle");
inputManager.addMapping("toggle", new KeyTrigger(keyInput.KEY_B));
System.out.println("Press 'B' to toggle the bug on/off.");
System.out.println("Node local rotation: "+node.getLocalRotation());
}
// please ignore the detail of this - it's just experimental code that creates a boxy robot-like entity for testing.
private Node createPlayerEntityNode(Material mat) {
String nodeIDSuffix = "42";
Node node = new Node("playerEntityNode-" + nodeIDSuffix);
Node topNode = new Node("playerEntityNode-" + nodeIDSuffix + "-scale");
topNode.setLocalTranslation(0, 2.5f, 0); // so that the rest of the nodes are shifted up above the feet.
topNode.setLocalScale(0.5f);
node.attachChild(topNode);
Node body = new Node("playerEntity-" + nodeIDSuffix + "-body");
topNode.attachChild(body);
Geometry headGeometry = new Geometry("playerEntity-" + nodeIDSuffix + "-head", new Box(0.5f, 0.5f, 0.35f));
headGeometry.center();
headGeometry.setMaterial(mat);
headGeometry.move(0, 0.3f, 0);
topNode.attachChild(headGeometry);
Geometry torsoGeometry = new Geometry("playerEntity-" + nodeIDSuffix + "-torso", new Box(0.7f, 1.1f, 0.4f));
torsoGeometry.center();
torsoGeometry.setMaterial(mat);
torsoGeometry.setLocalTranslation(0, -1.3f, 0);
body.attachChild(torsoGeometry);
Geometry leftLegGeometry = new Geometry("playerEntity-" + nodeIDSuffix + "-leftLeg", new Box(0.3f, 1.30f, 0.3f));
leftLegGeometry.center();
leftLegGeometry.setMaterial(mat);
leftLegGeometry.move(-0.35f, -3.7f, 0);
body.attachChild(leftLegGeometry);
Geometry rightLegGeometry = new Geometry("playerEntity-" + nodeIDSuffix + "-leftLeg", new Box(0.3f, 1.30f, 0.3f));
rightLegGeometry.center();
rightLegGeometry.setMaterial(mat);
rightLegGeometry.move(+0.35f, -3.7f, 0);
body.attachChild(rightLegGeometry);
return node;
}
private Material createLightingMaterial(ColorRGBA color) {
Material material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
material.setBoolean("UseMaterialColors", true);
material.setColor("Ambient", color);
material.setColor("Diffuse", color);
material.setColor("Specular", color);
material.setFloat("Shininess", 1.0f);
return material;
}
private void addAmbientLight() {
AmbientLight ambientLight = new AmbientLight(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
rootNode.addLight(ambientLight);
}
private void addDirectionalLight() {
DirectionalLight light = new DirectionalLight();
light.setColor(ColorRGBA.White);
light.setDirection(new Vector3f(-1, -1, -1));
rootNode.addLight(light);
}
}