[SOLVED] Better character control rotates extremely fast on tangent

I have a character with a better character control. I switched the chase cam to a camera node. I can rotate the camera node while being stationary and that works. I can move forward without moving the camera and that works. I can move the camera while moving forward and that works. pressing a or s or d makes the model spin at an extreme rate. pressing d for example should make the right side of the model, the world famous lisa, show her right side and move in that direction. I do understand what is going on but do not know how to fix it. the character control rotates to move the model in the direction but that also rotates the camera node target. rotating the camera node target causes the spinning. I have tried to find a way to get and then set the world rotation but that does not seem possible in JME. I have made something similar to this in unity and I had access to the world rotation. I need to be able to change the camera node target rotation back to the angle it was pointing before which would mean the world rotation not the local rotation. I am not sure how to accomplish this. I have edited down a code sample to what I think is the relevant code from the original file. If needed I can zip the whole directory and post it here. here is the code sample.

*** Edit. If I get the camera at just the right angle, nearly 90 the the side of lisa and press a or d then I can get the expected behavior. If the camera is more than a few degrees off the model will rotate way too fast.

public void simpleInitApp() {
    BulletAppState bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
    
    //landscape
    //lisa, has better character controler. camRotate is child node of lisa. camParent is child node of camRotate. 
    
    flyCam.setEnabled(false);
    camNode = new CameraNode("Camera Node", cam);
    camNode.setControlDir(ControlDirection.SpatialToCamera);
    camRotate = (Node) lisa.getChild("camRotate");
    camParent = (Node) lisa.getChild("camParent");
    camParent.attachChild(camNode);
    
    camNode.setLocalTranslation(new Vector3f(0, 0, 0));
    
    inputManager.addMapping("CharLeft", new KeyTrigger(KeyInput.KEY_A));
    inputManager.addMapping("CharRight", new KeyTrigger(KeyInput.KEY_D));
    inputManager.addMapping("CharForward", new KeyTrigger(KeyInput.KEY_W));
    inputManager.addMapping("CharBackward", new KeyTrigger(KeyInput.KEY_S));
    inputManager.addMapping("CharJump", new KeyTrigger(KeyInput.KEY_RETURN));
    inputManager.addMapping("CharAttack", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addMapping("toggleRotate", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    inputManager.addMapping("rotateRight", new MouseAxisTrigger(MouseInput.AXIS_X, true));
    inputManager.addMapping("rotateLeft", new MouseAxisTrigger(MouseInput.AXIS_X, false));
    inputManager.addMapping("rotateUp", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
    inputManager.addMapping("rotateDown", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
    inputManager.addListener(this, "CharLeft", "CharRight", "CharForward", "CharBackward", "CharJump", "CharAttack", "rotateRight", "rotateLeft", "toggleRotate", "rotateUp", "rotateDown");
}

public void simpleUpdate(float tpf) {
    Vector3f camDir = cam.getDirection().clone();
    Vector3f camLeft = cam.getLeft().clone();
    camDir.y = 0;
    camLeft.y = 0;
    camDir.normalizeLocal();
    camLeft.normalizeLocal();
    walkDirection.set(0, 0, 0);
    
    float [] angles = camRotate.getLocalRotation().toAngles(null);
    float [] anglesW= camRotate.getWorldRotation().toAngles(null);
    if (left)   {
        walkDirection.addLocal(camLeft);
    }
    if (right) {
        walkDirection.addLocal(camLeft.negate());
    }
    if (up) {
        walkDirection.addLocal(camDir);
        camRotate.getLocalRotation().fromAngles( angles[0], angles[1], 0 ).nlerp( Quaternion.ZERO.fromAngles( angles[0], 0, 0 ), .4f );
    }
    if (down) {
        walkDirection.addLocal(camDir.negate());
    }

    walkDirection.multLocal(25f).multLocal(tpf);
    character.setWalkDirection(walkDirection); 
    if ( !up ) camRotate.setLocalRotation( Quaternion.ZERO.fromAngles( angles[0], angles[1], 0 ) );
}

public void onAction(String binding, boolean value, float tpf) {
    if (binding.equals("CharLeft")) {
        if (value) left = true;
        else left = false;
    } else if (binding.equals("CharRight")) {
        if (value) right = true;
        else right = false;
    } else if (binding.equals("CharForward")) {
        if (value) up = true;
        else up = false;
    } else if (binding.equals("CharBackward")) {
        if (value) down = true;
        else down = false;
    } else if (binding.equals("CharJump"))
        character.jump();
    if (binding.equals("toggleRotate") && value) {
        rotate = true;
        inputManager.setCursorVisible(false);
    }
    if (binding.equals("toggleRotate") && !value) {
        rotate = false;
        inputManager.setCursorVisible(true);
    }
}

public void onAnalog(String name, float value, float tpf) {
    float [] angles = camRotate.getLocalRotation().toAngles(null);
    float speed = 1f;
    if (name.equals("rotateRight") && rotate) {
        camRotate.setLocalRotation( Quaternion.ZERO.fromAngles( angles[0], angles[1] + (speed * tpf), 0 ) );
    }
    if (name.equals("rotateLeft") && rotate) {
        camRotate.setLocalRotation( Quaternion.ZERO.fromAngles( angles[0], angles[1] + (-speed * tpf), 0 ) );
    }
    if (name.equals("rotateUp") && rotate) {
        camRotate.setLocalRotation( Quaternion.ZERO.fromAngles( angles[0] + (speed * tpf), angles[1], 0 ) );
    }
    if (name.equals("rotateDown") && rotate) {
        camRotate.setLocalRotation( Quaternion.ZERO.fromAngles( angles[0] + (-speed * tpf), angles[1], 0 ) );
    }
}

I would appreciate any insight. Thank you all in advance.

If you want the character to rotate and not the camera then counter rotate the camera when you rotate the character.

Edit: or decouple camera rotation from character rotation completely.

I tried using walkdirection and inverting it but that did not work.

Vector3f anglesWD = walkDirection.clone();
if ( left || right || down ) camRotate.setLocalRotation( Quaternion.ZERO.fromAngles( angles[0], ( 360 - anglesWD.y ), 0 ) );

in fact that just made it worse. Any Ideas?

so I tried

float [] lisaAnglesBefore = lisa.getWorldRotation().toAngles(null);
character.setWalkDirection(walkDirection);
float [] lisaAnglesAfter = lisa.getWorldRotation().toAngles(null);
float diff = lisaAnglesAfter[1] - lisaAnglesBefore[1];
if ( left || right || down ) camRotate.setLocalRotation( Quaternion.ZERO.fromAngles( angles[0], ( angles[1] - diff ), 0 ) );

Thinking that would work I tested it, but no. So I tried printing out the value of diff. The value was always 0.0 with no deviation. So I tried printing lisaAnglesBefore[1] and that has a range from -3.14 to +3.12. This is odd, it should be in degrees right? aside from that why would after - before always equal 0?

so i did a

System.out.println( lisaAnglesBefore[1] + " " + lisaAnglesAfter[1] + " " + diff );

while holding down the d key. and here is the output.

-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0
2.5764158 2.5764158 0.0
1.0056193 1.0056193 0.0
-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0
2.5764158 2.5764158 0.0
1.0056193 1.0056193 0.0
-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0
2.5764158 2.5764158 0.0
1.0056193 1.0056193 0.0
-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0
2.5764158 2.5764158 0.0
1.0056193 1.0056193 0.0
-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0
2.5764158 2.5764158 0.0
1.0056193 1.0056193 0.0
-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0
2.5764158 2.5764158 0.0
1.0056193 1.0056193 0.0
-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0
2.5764158 2.5764158 0.0
1.0056193 1.0056193 0.0
-0.56517696 -0.56517696 0.0
-2.1359732 -2.1359732 0.0

I was reading the javadoc and the javadoc for PhysicsCharacter is essentially empty.

1 Like

The javadoc for jme3-bullet is more complete than that for jme3-jbullet.

And the javadoc for Minie is better than either of them…

1 Like

What is your expectation of how the camera should move in relation to how the character moves?

Camera rotates, so does character? Character rotates separate from camera?

By the way, you are directly modifying the ZERO constant here. If anything else is using Quaternion.ZERO then it’s going to get your weird value.

You are also not really saving any characters over new Quaternion().fromAngles()… so might as well do it the correct way.

1 Like

@ mitm wrote

What is your expectation of how the camera should move in relation to how the character moves?

If you have ever played spell force, that is the general idea. Currently moving forward is working. Also spinning the camera while moving is working. Also spinning the camera while stationary is working. Hitting left, right or backwards is the problem. For right for example. I should see the right side of the model. The model should move to the right. The camera should continue pointing in the original direction. An object in the back should continue to be seen.

Camera rotates, so does character?

no, rotating the camera while the model is stationary should not rotate model. The model could be seen from the front.

Character rotates separate from camera?

almost, the model should rotate 90 degrees relative to the camera and travel 90 degrees relative to the camera. when left or right are pressed. The model should rotate 180 degrees, showing the front of the model. The model should then travel backwards relative to the camera. Of course the camera being a child of the model, should move with the model.

@pspeed Wait you lost me there. In my reading Quaternion.ZERO was the way to create a new empty Quaternion. Maybe I missed a constructor in the javadoc. But you said:

If anything else is using Quaternion.ZERO

shouldn’t that just be zeros for x,y,z and w with 1? Can that be modified? I’m surprised and do not know what to say. I will try the constructor with no arguments.

Quaternion.ZERO is not a constructor. It’s not going to create a new object every time you reference that field. Quaternion.ZERO is a static field. It contains one object that you are reusing over and over again… and stomping over its values with fromAngles() which modifies the values of the quaternion directly.

Quaternion q1 = Quaternion.ZERO.fromAngles(1, 2, 3);
Quaternion q2 = Quaternion.ZERO.fromAngles(4, 5, 6);
if( q1 == q2 ) {
    System.out.println("I've been very bad and have been abusing the constants.");
}
1 Like

lol, I will rewrite it. And I was just reread the javadoc and will use the correct constructor.

https://wiki.jmonkeyengine.org/jme3/rotate.html#troubleshooting-rotations

I have corrected the Quaternion.ZERO but diff still comes out to zero. I cannot find anything to use to counter rotate the camera.

That wasn’t your problem… it was just A problem.

You need to decouple the camera rotation from the character rotation and then when you want them to operate independently, change them independently. When you want them to operate together, synch them up.

Keep character yaw and pitch and camera yaw and pitch separate. Always build your quaternion from them as needed. Input will modify the appropriate yaw/pitch as needed… or both… or whatever. Then it’s just a logic problem.

I have come to a solution. It is not exactly what I wanted with using the previous world rotation and it does have a small amount of camera bounce in some situations. But I did come to a solution with something @mitm pointed out. For the sake of posterity here is the code at it currently sits:

package mygame;

import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.LoopMode;
import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Node;
import com.jme3.scene.control.CameraControl.ControlDirection;

/**
 * This is the Main Class of your Game. You should only do initialization here.
 * Move your Logic into AppStates or Controls
 * @author normenhansen
 */
public class Main extends SimpleApplication implements ActionListener, AnimEventListener, AnalogListener {
    
    public Node scene, lisa, camRotate, camParent;
    public CharacterControl character;
    private AnimChannel animationChannel;
    private AnimControl animationControl;
    private boolean left = false, right = false, up = false, down = false, collider = false, rotate = false;
    private Vector3f walkDirection = new Vector3f(0,0,0); // stop
    private float airTime = 0;
    public CameraNode camNode;
    public int camDist = 2;
    
    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        BulletAppState bulletAppState = new BulletAppState();
        if ( collider )
            bulletAppState.setDebugEnabled(true);
        stateManager.attach(bulletAppState);
        
        scene = (Node) assetManager.loadModel("Scenes/walking.j3o");
        rootNode.attachChild(scene);
        RigidBodyControl landscape;
        CollisionShape sceneShape = CollisionShapeFactory.createMeshShape(scene);
        landscape = new RigidBodyControl(sceneShape, 0);
        scene.addControl(landscape);
        bulletAppState.getPhysicsSpace().add(scene);
        
        lisa = (Node) assetManager.loadModel("Models/lisa/lisa_test2.j3o");
        CapsuleCollisionShape capsule = new CapsuleCollisionShape(.2f, 1.4f, 1);
        character = new CharacterControl(capsule, 0.05f);
        character.setJumpSpeed(20f);
        lisa.addControl(character);
        lisa.setLocalTranslation( new Vector3f( 0.0f, -5f, 0.0f )  );
        character.setPhysicsLocation(new Vector3f(0, 1, 0));
        scene.attachChild(lisa);
        bulletAppState.getPhysicsSpace().add(character);
        
        animationControl = lisa.getChild("Female_elegantsuit01").getControl(AnimControl.class);
        animationControl.addListener(this);
        animationChannel = animationControl.createChannel();
        
        flyCam.setEnabled(false);
        camNode = new CameraNode("Camera Node", cam);
        camNode.setControlDir(ControlDirection.SpatialToCamera);
        camRotate = (Node) lisa.getChild("camRotate");
        camParent = (Node) lisa.getChild("camParent");
        camParent.attachChild(camNode);
        
        camNode.setLocalTranslation(new Vector3f(0, 0, 0));
//        camNode.lookAt(camRotate.getLocalTranslation(), Vector3f.UNIT_Y);
        
        inputManager.addMapping("CharLeft", new KeyTrigger(KeyInput.KEY_A));
        inputManager.addMapping("CharRight", new KeyTrigger(KeyInput.KEY_D));
        inputManager.addMapping("CharForward", new KeyTrigger(KeyInput.KEY_W));
        inputManager.addMapping("CharBackward", new KeyTrigger(KeyInput.KEY_S));
        inputManager.addMapping("CharJump", new KeyTrigger(KeyInput.KEY_RETURN));
        inputManager.addMapping("CharAttack", new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addMapping("toggleRotate", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addMapping("rotateRight", new MouseAxisTrigger(MouseInput.AXIS_X, true));
        inputManager.addMapping("rotateLeft", new MouseAxisTrigger(MouseInput.AXIS_X, false));
        inputManager.addMapping("rotateUp", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
        inputManager.addMapping("rotateDown", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
        inputManager.addMapping("wheelUp", new MouseAxisTrigger(MouseInput.AXIS_WHEEL,false));
        inputManager.addMapping("wheelDown", new MouseAxisTrigger(MouseInput.AXIS_WHEEL,true));
        inputManager.addListener(this, "CharLeft", "CharRight");
        inputManager.addListener(this, "CharForward", "CharBackward");
        inputManager.addListener(this, "CharJump", "CharAttack");
        inputManager.addListener(this, "rotateRight", "rotateLeft", "toggleRotate");
        inputManager.addListener(this, "rotateUp", "rotateDown");
        inputManager.addListener(this, "wheelUp", "wheelDown");
    }

    @Override
    public void simpleUpdate(float tpf) {
        Vector3f camDir = cam.getDirection().clone();
        Vector3f camLeft = cam.getLeft().clone();
        camDir.y = 0;
        camLeft.y = 0;
        camDir.normalizeLocal();
        camLeft.normalizeLocal();
        walkDirection.set(0, 0, 0);
        camParent.setLocalTranslation( 0, 0, -camDist );
        float [] angles = camRotate.getLocalRotation().toAngles(null);
        float [] anglesInv = camRotate.getLocalRotation().inverse().toAngles(null);  
        if ( left || right ) { up = false; down = false; } 
        if (left)   {
            walkDirection.addLocal(camLeft);
            if ( !rotate ) camRotate.setLocalRotation( new Quaternion().fromAngles( angles[0], FastMath.PI * 3 / 2, 0 ) );
            if ( rotate ) camRotate.getLocalRotation().fromAngles( angles[0], FastMath.PI * 3 / 2, 0 ).nlerp( new Quaternion().fromAngles( angles[0], angles[1], 0 ), .4f );
        }
        if (right) {
            walkDirection.addLocal(camLeft.negate());
            if ( !rotate ) camRotate.setLocalRotation( new Quaternion().fromAngles( angles[0], FastMath.PI / 2, 0 ) );
            if ( rotate ) camRotate.getLocalRotation().fromAngles( angles[0], FastMath.PI / 2, 0 ).nlerp( new Quaternion().fromAngles( angles[0], angles[1], 0 ), .4f );
        }
        if (up) {
            walkDirection.addLocal(camDir);
            if ( rotate ) camRotate.getLocalRotation().fromAngles( angles[0], 0, 0 ).nlerp( new Quaternion().fromAngles( angles[0], angles[1], 0 ), .4f );
            if ( !rotate ) camRotate.getLocalRotation().fromAngles( angles[0], 0, 0 ).nlerp( new Quaternion().fromAngles( angles[0], anglesInv[1], 0 ), .1f );
        }
        if (down) {
            walkDirection.addLocal(camDir.negate());
            if ( !rotate ) camRotate.setLocalRotation( new Quaternion().fromAngles( angles[0], FastMath.PI, 0 ) );
            if ( rotate ) camRotate.getLocalRotation().fromAngles( angles[0], FastMath.PI, 0 ).nlerp( new Quaternion().fromAngles( angles[0], angles[1], 0 ), .4f );
        }

        if (!character.onGround()) { // use !character.isOnGround() if the character is a BetterCharacterControl type.
            airTime += tpf;
        } else {
            airTime = 0;
        }
        try {
            if (walkDirection.lengthSquared() == 0) { //Use lengthSquared() (No need for an extra sqrt())
                if ( !"idle-baked".equals( animationChannel.getAnimationName() ) ) {
                  animationChannel.setAnim("idle-baked", 1f);
                }
            } else {
                character.setViewDirection(walkDirection);
                if (airTime > .3f) {
                  if (!"idle-baked".equals(animationChannel.getAnimationName())) {
                    animationChannel.setAnim("idle-baked");
                  }
                } else if (!"walking-baked".equals(animationChannel.getAnimationName())) {
                  animationChannel.setAnim("walking-baked", 0.2f); //
                  animationChannel.setLoopMode( LoopMode.Loop );
                }
            }
        } catch ( NullPointerException e ) {}
        
        walkDirection.multLocal(25f).multLocal(tpf);// The use of the first multLocal here is to control the rate of movement multiplier for character walk speed. The second one is to make sure the character walks the same speed no matter what the frame rate is.
        character.setWalkDirection(walkDirection); // THIS IS WHERE THE WALKING HAPPENS
    }

    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }

    @Override
    public void onAction(String binding, boolean value, float tpf) {
        if ( binding.equals( "CharLeft" ) ) 
            left = value;
        if ( binding.equals( "CharRight" ) ) 
            right = value;
        if ( binding.equals( "CharForward" ) ) 
            up = value;
        if ( binding.equals( "CharBackward" ) ) 
            down = value;
        if ( binding.equals( "CharJump" ) )
            character.jump();
        if ( binding.equals( "toggleRotate" ) ) {
            rotate = value;
            inputManager.setCursorVisible(!value);
        }
        if ( binding.equals( "wheelUp" ) && value ) 
            camDist--;
        if ( binding.equals( "wheelDown" ) && value ) 
            camDist++;
        if ( camDist < 2 ) camDist = 2;
        if ( camDist > 10 ) camDist = 10;
//        if (binding.equals("CharAttack"))
//            attack();
        
    }
    
    @Override
    public void onAnalog(String name, float value, float tpf) {
        float [] angles = camRotate.getLocalRotation().toAngles(null);
        float rotateSpeed = 1f;
        if (name.equals("CharForward")) {
            up = true;
        }
        if (name.equals("CharBackward")) {
            down = true;
        }
        if (name.equals("rotateRight") && rotate) {
            camRotate.setLocalRotation( new Quaternion().fromAngles( angles[0], angles[1] + (rotateSpeed * tpf), 0 ) );
        }
        if (name.equals("rotateLeft") && rotate) {
            camRotate.setLocalRotation( new Quaternion().fromAngles( angles[0], angles[1] + (-rotateSpeed * tpf), 0 ) );
        }
        if (name.equals("rotateUp") && rotate) {
            camRotate.setLocalRotation( new Quaternion().fromAngles( angles[0] + (rotateSpeed * tpf), angles[1], 0 ) );
        }
        if (name.equals("rotateDown") && rotate) {
            camRotate.setLocalRotation( new Quaternion().fromAngles( angles[0] + (-rotateSpeed * tpf), angles[1], 0 ) );
        }
    }

    @Override
    public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {}

    @Override
    public void onAnimChange(AnimControl arg0, AnimChannel arg1, String arg2) {}
}
1 Like