[SOLVED] Having BetterCharacterControl control move in the direction of the camera, at a constant speed

So I’ve been having this problem for a few days where I can’t get the physicsCharacter to move in the direction the camera is facing, well I can sort of do it. I’ve been playing with jme3test.bullet.TestBetterCharacter and I’ve gotten the character to move in the direction the camera is facing, but depending on the height of the camera the character will speed up (at lowest height) or come to a complete stop (when the camera is directly above the character).

I want the character to move at a constant speed no matter the height of the camera. I’ve looked in as many examples of BetterCharacterControl & CharacterControl as I can find like TestWalkingChar, HelloCollision, HelloChaseCam etc and they all exhibit the same behaviour. I’m assuming its my poor maths ability that is the biggest factor in my difficultly overcoming this problem. Could anyone please provide me with some pointers? I’m certain someone has figured this out.

This is the code but probably only simpleUpdate() is important

/*
 * Copyright (c) 2009-2012 jMonkeyEngine All rights reserved. <p/>
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer. <p/> * Redistributions
 * in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other
 * materials provided with the distribution. <p/> * Neither the name of
 * 'jMonkeyEngine' nor the names of its contributors may be used to endorse or
 * promote products derived from this software without specific prior written
 * permission. <p/> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package jme3test.bullet;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.debug.DebugTools;
import com.jme3.input.ChaseCamera;
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.material.Material;
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.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.control.CameraControl;
import com.jme3.scene.control.CameraControl.ControlDirection;
import com.jme3.scene.shape.Sphere;
import com.jme3.system.AppSettings;

/**
 * A walking physical character followed by a 3rd person camera. (No animation.)
 *
 * @author normenhansen, zathras
 */
public class TestEvenBetterCharacterThatWorks extends SimpleApplication implements ActionListener {

    private BulletAppState bulletAppState;
    private BetterCharacterControl physicsCharacter;
    private Node characterNode;
    private CameraNode camNode;
    private ChaseCamera chaseCam;
    
    private Node model;
    
    private Quaternion currentRotation;
    private Quaternion pastRotation;
    private Float differenceRotation;
    
    boolean rotate = false;
    private Vector3f walkDirection = new Vector3f(0, 0, 0);
    private Vector3f viewDirection = new Vector3f(-1, 0, 0);
    boolean leftStrafe = false, rightStrafe = false, forward = false, backward = false,
            leftRotate = false, rightRotate = false, mRotateLeft = false, mRotateRight = false;
    private Vector3f normalGravity = new Vector3f(0, -9.81f, 0);
    private Geometry planet;

    public static void main(String[] args) {
        TestEvenBetterCharacterThatWorks app = new TestEvenBetterCharacterThatWorks();
        AppSettings settings = new AppSettings(true);
        settings.setRenderer(AppSettings.LWJGL_OPENGL3);
        settings.setAudioRenderer(AppSettings.LWJGL_OPENAL);
        app.setSettings(settings);
        app.start();
    }

    @Override
    public void simpleInitApp() {
        
        currentRotation = new Quaternion();
        pastRotation = new Quaternion();
        //setup keyboard mapping
        setupKeys();

        // activate physics
        bulletAppState = new BulletAppState();
        stateManager.attach(bulletAppState);
        bulletAppState.setDebugEnabled(true);

        // init a physics test scene
        PhysicsTestHelper.createPhysicsTestWorldSoccer(rootNode, assetManager, bulletAppState.getPhysicsSpace());
        PhysicsTestHelper.createBallShooter(this, rootNode, bulletAppState.getPhysicsSpace());
        setupPlanet();

        // Create a node for the character model
        characterNode = new Node("character node");
        characterNode.setLocalTranslation(new Vector3f(4, 5, 2));

        // Add a character control to the node so we can add other things and
        // control the model rotation
        physicsCharacter = new BetterCharacterControl(0.3f, 2.5f, 8f);
        characterNode.addControl(physicsCharacter);
        getPhysicsSpace().add(physicsCharacter);

        // Load model, attach to character node
        model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o");
        model.setLocalScale(1.50f);
        characterNode.attachChild(model);

        // Add character node to the rootNode
        rootNode.attachChild(characterNode);

        // Initiate the camera
        chaseCam = new ChaseCamera(cam, characterNode, inputManager);
        characterNode.addControl(chaseCam);
        chaseCam.setDragToRotate(false);
        
        // Without this the mouse doesn't stay locked??
        flyCam.setEnabled(true);


    }

    @Override
    public void simpleUpdate(float tpf) {
        
        // Set the character's directions to that of the camera
        Vector3f modelForwardDir = cam.getDirection().clone().multLocal(0.1f);
        Vector3f modelLeftDir = cam.getLeft().clone().multLocal(0.1f);
        
        // allow model to jump
        modelForwardDir.y = 0;
        modelLeftDir.y = 0;
        
        
        // Apply planet gravity to character if close enough (see below)
        //fpsText.setText("Touch da ground = " + physicsCharacter.isOnGround());

        checkPlanetGravity();


        // WalkDirection is global!
        // You *can* make your character fly with this.
        walkDirection.set(0, 0, 0);
        if (leftStrafe) {
            walkDirection.addLocal(modelLeftDir.mult(30));
        } else if (rightStrafe) {
            walkDirection.addLocal(modelLeftDir.negate().multLocal(30));
        }
        if (forward) {
            walkDirection.addLocal(modelForwardDir.mult(30));
        } else if (backward) {
            walkDirection.addLocal(modelForwardDir.negate().multLocal(30));
        }
        

        physicsCharacter.setWalkDirection(walkDirection);
        
        fpsText.setText(Float.toString(walkDirection.x));


        // ViewDirection is local to characters physics system!
        // The final world rotation depends on the gravity and on the state of
        // setApplyPhysicsLocal()
        if (leftRotate) {
            Quaternion rotateL = new Quaternion().fromAngleAxis(FastMath.PI * tpf, Vector3f.UNIT_Y);
            rotateL.multLocal(viewDirection);
        } else if (rightRotate) {
            Quaternion rotateR = new Quaternion().fromAngleAxis(-FastMath.PI * tpf, Vector3f.UNIT_Y);
            rotateR.multLocal(viewDirection);
        }
        
        if (mRotateLeft) {
            //Quaternion rotateL = new Quaternion().fromAngleAxis(FastMath.PI * tpf, Vector3f.UNIT_Y);
            //rotateL.multLocal(viewDirection);
        } else if (mRotateRight) {
            //Quaternion rotateR = new Quaternion().fromAngleAxis(-FastMath.PI * tpf, Vector3f.UNIT_Y);
            //rotateR.multLocal(viewDirection);
        }
        physicsCharacter.setViewDirection(viewDirection);
        //model.lookAt(walkDirection, cam.getUp());
        //fpsText.setText("Touch da ground = " + physicsCharacter.isOnGround());
        if (!lockView) {
            cam.lookAt(characterNode.getWorldTranslation().add(new Vector3f(0, 2, 0)), Vector3f.UNIT_Y);
        }
        

        
    }

    private void setupPlanet() {
        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
        //immovable sphere with mesh collision shape
        Sphere sphere = new Sphere(64, 64, 20);
        planet = new Geometry("Sphere", sphere);
        planet.setMaterial(material);
        planet.setLocalTranslation(30, -15, 30);
        planet.addControl(new RigidBodyControl(new MeshCollisionShape(sphere), 0));
        rootNode.attachChild(planet);
        getPhysicsSpace().add(planet);
    }

    private void checkPlanetGravity() {
        Vector3f planetDist = planet.getWorldTranslation().subtract(characterNode.getWorldTranslation());
        if (planetDist.length() < 24) {
            physicsCharacter.setGravity(planetDist.normalizeLocal().multLocal(9.81f));
        } else {
            physicsCharacter.setGravity(normalGravity);
        }
    }

    private PhysicsSpace getPhysicsSpace() {
        return bulletAppState.getPhysicsSpace();
    }
    
    private AnalogListener analogListener = new AnalogListener() {
    @Override
    public void onAnalog(String binding, float value, float tpf) {
        if (binding.equals("mouseRotateLeft")) {
            
            
            Quaternion rotateL = new Quaternion().fromAngleAxis(FastMath.PI * tpf * value, Vector3f.UNIT_Y);
            rotateL.multLocal(viewDirection);

        } else if (binding.equals("mouseRotateRight")) {
            Quaternion rotateR = new Quaternion().fromAngleAxis(-FastMath.PI * tpf * value, Vector3f.UNIT_Y);
            rotateR.multLocal(viewDirection);
        }
    }
};

    public void onAction(String binding, boolean value, float tpf) {
        if (binding.equals("Strafe Left")) {
            if (value) {
                leftStrafe = true;
            } else {
                leftStrafe = false;
            }
        } else if (binding.equals("Strafe Right")) {
            if (value) {
                rightStrafe = true;
            } else {
                rightStrafe = false;
            }
        } else if (binding.equals("Rotate Left")) {
            if (value) {
                leftRotate = true;
            } else {
                leftRotate = false;
            }
        } else if (binding.equals("Rotate Right")) {
            if (value) {
                rightRotate = true;
            } else {
                rightRotate = false;
            }
        } else if (binding.equals("Walk Forward")) {
            if (value) {
                forward = true;
            } else {
                forward = false;
            }
        } else if (binding.equals("Walk Backward")) {
            if (value) {
                backward = true;
            } else {
                backward = false;
            }
        } else if (binding.equals("Jump")) {
            physicsCharacter.jump();
        } else if (binding.equals("Duck")) {
            if (value) {
                physicsCharacter.setDucked(true);
            } else {
                physicsCharacter.setDucked(false);
            }
        } else if (binding.equals("Lock View")) {
            if (value && lockView) {
                lockView = false;
            } else if (value && !lockView) {
                lockView = true;
            }
            flyCam.setEnabled(!lockView);
            camNode.setEnabled(lockView);
        }         if (binding.equals("mouseRotateLeft")) {
            if (value) {
                mRotateLeft = true;
                
            } else {
                mRotateLeft = false;
            }
        } else if (binding.equals("mouseRotateRight")) {
            if (value) {
                mRotateRight = true;
            } else {
                mRotateRight = false;
            }
        }
    }
    private boolean lockView = false;

    private void setupKeys() {
        inputManager.addMapping("Strafe Left",
                new KeyTrigger(KeyInput.KEY_A),
                new KeyTrigger(KeyInput.KEY_Z));
        inputManager.addMapping("Strafe Right",
                new KeyTrigger(KeyInput.KEY_D),
                new KeyTrigger(KeyInput.KEY_X));
        inputManager.addMapping("Rotate Left",
                new KeyTrigger(KeyInput.KEY_Q),
                new KeyTrigger(KeyInput.KEY_LEFT));
        inputManager.addMapping("Rotate Right",
                new KeyTrigger(KeyInput.KEY_E),
                new KeyTrigger(KeyInput.KEY_RIGHT));
        inputManager.addMapping("Walk Forward",
                new KeyTrigger(KeyInput.KEY_W),
                new KeyTrigger(KeyInput.KEY_UP));
        inputManager.addMapping("Walk Backward",
                new KeyTrigger(KeyInput.KEY_S),
                new KeyTrigger(KeyInput.KEY_DOWN));
        inputManager.addMapping("Jump",
                new KeyTrigger(KeyInput.KEY_F),
                new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addMapping("Duck",
                new KeyTrigger(KeyInput.KEY_G),
                new KeyTrigger(KeyInput.KEY_LSHIFT),
                new KeyTrigger(KeyInput.KEY_RSHIFT));
        inputManager.addMapping("Lock View",
                new KeyTrigger(KeyInput.KEY_RETURN));
        inputManager.addMapping("mouseRotateLeft", new MouseAxisTrigger(MouseInput.AXIS_X, true));
        inputManager.addMapping("mouseRotateRight", new MouseAxisTrigger(MouseInput.AXIS_X, false));
        inputManager.addListener(analogListener, "mouseRotateLeft", "mouseRotateRight");
        
        
        
        
        inputManager.addListener(this, "Strafe Left", "Strafe Right");
        inputManager.addListener(this, "Rotate Left", "Rotate Right");
        inputManager.addListener(this, "Walk Forward", "Walk Backward");
        inputManager.addListener(this, "Jump", "Duck", "Lock View");
    }

    @Override
    public void simpleRender(RenderManager rm) {
    }
}

I was thinking it could be done with a spatial in front of the character and having the character move in its direction like a carrot on a stick, but then wouldn’t that spatial would just move with the character and I’d have the same problem :confused:

Not sure why this post is so hard for folks to find… it’s right at the top and stickied:

…when I join a new code-related form “how to post code blocks” is usually the first thing I look for.

Anyway, hope it helps clean up your post so someone might help you without going crazy.

Thanks pspeed, I was wondering how to do it while I was making my post. Maybe something can be added to the post formatting tools

The length of the walk direction vector defines the speed.

Basically you could call

betterCharacterControl.setWalkDirection(cam.getDirection().mult(tpf * speed));

Okay I think the problem is less the speed but more the direction of the vector? So when the camera is above the character, he is not so much at a complete stop as he is trying to force his way through the ground (well would be if I hadn’t set the y value to 0), and when the camera is at its lowest point the character moves straight ahead. I still can’t work it out though how to get him to always move and at the same speed.

you mean that if your camera is above the guy, the guy wants to go through the floor and therefore cannot go along x or z?

That basically means that the direction vector is pointing downwards (negative y).
You can make a direction on the xz-plane by doing something like:

Vector3f rawdir = camera.getDirection();
Vector3f xzdir = new Vector3f(rawdir.x, 0f, rawdir.z).normalizeLocal();  //ignores y, still a vector with length == 1f

Is that what you meant?

1 Like

Omg thank you! I was trying to do that earlier, I tried to normalise it with walkDirection.normalise() and it didn’t work, but that works perfectly!

1 Like

Ah pooh, this works until he is on the planet, then it breaks horribly, any ideas?

Edit: Sorry I shouldn’t get you to do all my work for me, I can probably work it out from here. Thanks so much everyone!

I am guessing because your planet does not use the global xz-plane, but a tangent plane of the sphere. Calculating that is more difficult, and I can’t just write a quick two-liner for that, but I’m sure you can figure that out yourself.

1 Like

Hint: project the camera direction onto the vertical direction, subtract that from the camera direction, then normalize the result.

1 Like

Thank you! This is what I came up with:

            Vector3f rawdir = cam.getDirection();
            Vector3f hrztldir = new Vector3f(rawdir.subtract(rawdir.project(physicsCharacter.getGravity().negate())).normalize());
            walkDirection.addLocal(hrztldir.mult(10));
            physicsCharacter.setViewDirection(walkDirection);

Much less catastrophic on the planet but still some odd behaviour, I think the problem is elsewhere now though, I think I have to get the camera to adjust properly to the planet.

1 Like