Movement control

Hi all,



I've been toying around with the physics library (for reference, I got it from SVN) and it looks great.



However, for my program I'm working on, I need to be able to control objects more or less more or less the way "I want".



I've been trying to implement movement through 3 systems; velocity, force and surfacemotion. So far, velocity resulted in the best behavior, but I wouldn't be surprised if the results for the other 2 were better with a better algorithm.



The points that I'm having problems with are the following:

  • When I move an object over the terrain, it shouldn't get 'airborne' when it hits a bump (just as a human doesn't get airborne when he walks over a small bump).
  • when 2 objects collide, the object that was standing still (that is a DynamicPhysicsNode but just isn't moving at the time) should slightly move aside to let the other object pass. Block the moving object altogether is valid as well, as long as the moving object doesn't 'drag' the still object away. Would be great if it could be specified per object
  • moving object doesn't get airborne when it collides with a still object.



    I've made a test case with 1 object that tries to move towards the position when you left click on the terrain

    100 other objects that are put in a grid (before falling down)



    F5 enables/disables the 100 objects trying to get back to the original grid (but then on the terrain instead of in the air)

    F6 toggles movement style. it rotates between velocity, force or surfacemotion. For the user controlled object and the 100 others when it is enabled (with F5)

    F7 moves the center of mass of the objects from the center to the bottom and back when pressed again



    The code is loosely based on the TestGenerateTerrain from the Physics library and thus needs the jmetest library for images for the terrain (should be easy to solve if not present).



    The problem seems pretty common to me, yet I've been struggling with it  :expressionless:



    I'm looking forward to any suggestions and feedback.



    Thanks in advance!


package YawnPhysics;

import javax.swing.ImageIcon;

import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.image.Texture.ApplyMode;
import com.jme.image.Texture.CombinerFunctionRGB;
import com.jme.image.Texture.CombinerOperandRGB;
import com.jme.image.Texture.CombinerScale;
import com.jme.image.Texture.CombinerSource;
import com.jme.image.Texture.MagnificationFilter;
import com.jme.image.Texture.MinificationFilter;
import com.jme.image.Texture.WrapMode;
import com.jme.input.InputHandler;
import com.jme.input.KeyInput;
import com.jme.light.DirectionalLight;
import com.jme.scene.state.CullState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.CullState.Face;
import com.jme.util.TextureManager;
import com.jmex.physics.DynamicPhysicsNode;
import com.jmex.physics.StaticPhysicsNode;
import com.jmex.physics.util.SimplePhysicsGame;
import com.jmex.terrain.TerrainPage;
import com.jmex.terrain.util.FaultFractalHeightMap;
import com.jmex.terrain.util.ProceduralTextureGenerator;

import com.jme.input.MouseInput;
import com.jme.input.action.InputAction;
import com.jme.input.action.InputActionEvent;
import com.jme.intersection.BoundingPickResults;
import com.jme.intersection.PickResults;
import com.jme.math.Ray;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.Text;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Sphere;
import com.jme.scene.state.BlendState;

import com.jme.system.DisplaySystem;
import com.jmetest.physics.TestGenerateTerrain;
import com.jmex.physics.material.Material;
import com.jmex.terrain.util.BresenhamTerrainPicker;
import java.util.logging.Level;
import java.util.logging.Logger;

public class YawnPhysics extends SimplePhysicsGame {

    Node terrainNode;
    DynamicPhysicsNode movingDPN;
    final Vector3f targetloc = new Vector3f();
    DynamicPhysicsNode[] otherDPN;
    Vector3f[] otherLoc;
    boolean otherHoldPosition = false;
    MovementStyle movementStyle = MovementStyle.VELOCITY;
    boolean centeredMasses = true;
    Text[] labels;

    @Override
    protected void simpleInitGame() {

        terrainNode = new Node("terrainNode");
        rootNode.attachChild(terrainNode);

        final StaticPhysicsNode staticNode = getPhysicsSpace().createStaticNode();
        Spatial terrain = createTerrain();
        staticNode.attachChild(terrain);
        staticNode.getLocalTranslation().set(0, -150, 0);
        staticNode.generatePhysicsGeometry();
        terrainNode.attachChild(staticNode);

        final StaticPhysicsNode wallStaticNode = getPhysicsSpace().createStaticNode();
        Spatial wall = createWall();
        wallStaticNode.attachChild(wall);
        wallStaticNode.getLocalTranslation().set(-30, -20, 0);
        wallStaticNode.generatePhysicsGeometry();
        terrainNode.attachChild(wallStaticNode);

        showPhysics = true;

        MouseInput.get().setCursorVisible(true);

        //new code
        labels = new Text[3];
        for (int i = 0; i < labels.length; i++) {
            labels[i] = Text.createDefaultTextLabel("label" + i, "label" + i);
            labels[i].setLocalTranslation(new Vector3f(0, i * 20 + 10, 1));
            rootNode.attachChild(labels[i]);
        }

        input.addAction(new InputAction() {

            public void performAction(InputActionEvent evt) {
                if (evt.getTriggerPressed()) {
                    otherHoldPosition = !otherHoldPosition;
                }
            }
        }, InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_F5, InputHandler.AXIS_NONE, false);

        input.addAction(new InputAction() {

            public void performAction(InputActionEvent evt) {
                if (evt.getTriggerPressed()) {
                    switch (movementStyle) {
                        case VELOCITY:
                            movementStyle = MovementStyle.FORCE;
                            break;
                        case FORCE:
                            movementStyle = movementStyle.SURFACEMOTION;
                            break;
                        case SURFACEMOTION:
                            movingDPN.getMaterial().setSurfaceMotion(Vector3f.ZERO);
                            for (int i = 0; i < otherDPN.length; i++) {
                                otherDPN[i].getMaterial().setSurfaceMotion(Vector3f.ZERO);
                            }
                        default:
                            movementStyle = movementStyle.VELOCITY;
                    }
                }
            }
        }, InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_F6, InputHandler.AXIS_NONE, false);

        input.addAction(new InputAction() {

            public void performAction(InputActionEvent evt) {
                if (evt.getTriggerPressed()) {
                    centeredMasses = !centeredMasses;
                    Vector3f position = new Vector3f(0, 0, 0);
                    if (!centeredMasses) {
                        position.y = -0.5f;
                    }
                    movingDPN.setCenterOfMass(position);
                    for (int i = 0; i < otherDPN.length; i++) {
                        otherDPN[i].setCenterOfMass(position);
                    }
                }
            }
        }, InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_F7, InputHandler.AXIS_NONE, false);

        createSelection();

        movingDPN = createMovingObject(new Vector3f(-2, 0, -2));

        otherDPN = new DynamicPhysicsNode[100];
        otherLoc = new Vector3f[100];
        int k = 0;
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                otherLoc[k] = new Vector3f(i * 2, 0, j * 2);
                otherDPN[k] = createMovingObject(otherLoc[k]);
                k++;
            }
        }
    }

    private Spatial createTerrain() {
        DirectionalLight dl = new DirectionalLight();
        dl.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
        dl.setDirection(new Vector3f(1, -0.5f, 1));
        dl.setEnabled(true);
        lightState.attach(dl);

        DirectionalLight dr = new DirectionalLight();
        dr.setEnabled(true);
        dr.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
        dr.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
        dr.setDirection(new Vector3f(0.5f, -0.5f, 0));

        lightState.attach(dr);

        display.getRenderer().setBackgroundColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1));

        FaultFractalHeightMap heightMap = new FaultFractalHeightMap(257, 32, 0, 255,
                0.75f, 3);
        Vector3f terrainScale = new Vector3f(10, 1, 10);
        heightMap.setHeightScale(0.001f);
        TerrainPage page = new TerrainPage("Terrain", 33, heightMap.getSize(), terrainScale,
                heightMap.getHeightMap());
        page.setDetailTexture(1, 16);

        CullState cs = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
        cs.setCullFace(Face.Back);
        cs.setEnabled(true);
        page.setRenderState(cs);

        ProceduralTextureGenerator pt = new ProceduralTextureGenerator(heightMap);
        pt.addTexture(new ImageIcon(TestGenerateTerrain.class.getClassLoader().getResource(
                "jmetest/data/texture/grassb.png")), -128, 0, 128);
        pt.addTexture(new ImageIcon(TestGenerateTerrain.class.getClassLoader().getResource(
                "jmetest/data/texture/dirt.jpg")), 0, 128, 255);
        pt.addTexture(new ImageIcon(TestGenerateTerrain.class.getClassLoader().getResource(
                "jmetest/data/texture/highest.jpg")), 128, 255, 384);

        pt.createTexture(512);

        TextureState ts = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
        ts.setEnabled(true);
        Texture t1 = TextureManager.loadTexture(pt.getImageIcon().getImage(),
                MinificationFilter.Trilinear, MagnificationFilter.Bilinear, true);
        ts.setTexture(t1, 0);

        Texture t2 = TextureManager.loadTexture(TestGenerateTerrain.class.getClassLoader().
                getResource("jmetest/data/texture/Detail.jpg"),
                MinificationFilter.Trilinear, MagnificationFilter.Bilinear);
        ts.setTexture(t2, 1);
        t2.setWrap(WrapMode.Repeat);

        t1.setApply(ApplyMode.Combine);
        t1.setCombineFuncRGB(CombinerFunctionRGB.Modulate);
        t1.setCombineSrc0RGB(CombinerSource.CurrentTexture);
        t1.setCombineOp0RGB(CombinerOperandRGB.SourceColor);
        t1.setCombineSrc1RGB(CombinerSource.PrimaryColor);
        t1.setCombineOp1RGB(CombinerOperandRGB.SourceColor);
        t1.setCombineScaleRGB(CombinerScale.One);

        t2.setApply(ApplyMode.Combine);
        t2.setCombineFuncRGB(CombinerFunctionRGB.AddSigned);
        t2.setCombineSrc0RGB(CombinerSource.CurrentTexture);
        t2.setCombineOp0RGB(CombinerOperandRGB.SourceColor);
        t2.setCombineSrc1RGB(CombinerSource.Previous);
        t2.setCombineOp1RGB(CombinerOperandRGB.SourceColor);
        t2.setCombineScaleRGB(CombinerScale.One);
        page.setRenderState(ts);

        return page;
    }

    private Spatial createWall() {
        Box b = new Box("Wall", null, 2, 10, 30);
        b.setModelBound(new BoundingBox());
        b.updateModelBound();
        b.setDefaultColor(new ColorRGBA(0.5f, 0, 0, 0.5f));
        b.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
        b.setLightCombineMode(Spatial.LightCombineMode.Off);
        BlendState bs = DisplaySystem.getDisplaySystem().getRenderer().createBlendState();
        bs.setBlendEnabled(true);
        bs.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
        bs.setDestinationFunction(BlendState.DestinationFunction.One);
        bs.setTestEnabled(true);
        bs.setTestFunction(BlendState.TestFunction.GreaterThan);
        bs.setEnabled(true);
        b.setRenderState(bs);

        return b;
    }

    private DynamicPhysicsNode createMovingObject(Vector3f location) {
        if (location == null) {
            location = new Vector3f();
        }
        Spatial spatial = new Box("meshbox", new Vector3f(), 0.5f, 0.5f, 0.5f);
        spatial.setModelBound(new BoundingBox());
        spatial.updateModelBound();

        DynamicPhysicsNode dpn = getPhysicsSpace().createDynamicNode();
        dpn.attachChild(spatial);
        dpn.generatePhysicsGeometry();
        dpn.computeMass();
        dpn.setMaterial(new Material());
        dpn.setCenterOfMass(new Vector3f(0, -0.5f, 0));

        dpn.getLocalTranslation().set(location);
        rootNode.attachChild(dpn);
        return dpn;
    }

    private void createSelection() {
        Sphere s = new Sphere("Selection", 11, 11, 1);
        s.setDefaultColor(new ColorRGBA(0, 128, 128, 0.1f));
        s.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
        s.setLightCombineMode(Spatial.LightCombineMode.Off);

        BlendState bs = DisplaySystem.getDisplaySystem().getRenderer().createBlendState();
        bs.setBlendEnabled(true);
        bs.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
        bs.setDestinationFunction(BlendState.DestinationFunction.One);
        bs.setTestEnabled(true);
        bs.setTestFunction(BlendState.TestFunction.GreaterThan);
        bs.setEnabled(true);
        s.setRenderState(bs);

        Node target = new Node();
        target.attachChild(s);
        target.setLocalTranslation(targetloc); //from now on, target will be where targetloc is pointing at
        rootNode.attachChild(target);
    }

    @Override
    protected void simpleUpdate() {
        cameraInputHandler.setEnabled(MouseInput.get().isButtonDown(1));

        labels[0].print("F5 toggle other hold, now " + otherHoldPosition);
        labels[1].print("F6 change movement style, now " + movementStyle.toString());
        labels[2].print("F7 toggle centered masses, now " + centeredMasses);

        updateTarget();

        updateMovement(movingDPN, targetloc, true);
        for (int i = 0; i < otherDPN.length; i++) {
            updateMovement(otherDPN[i], otherLoc[i], otherHoldPosition);
        }
    }

    private void updateMovement(final DynamicPhysicsNode dpn, final Vector3f target, boolean active) {
        switch (movementStyle) {
            case VELOCITY:
                updateMovementVelocity(dpn, target, active);
                break;
            case FORCE:
                updateMovementForce(dpn, target, active);
                break;
            case SURFACEMOTION:
                updateMovementSurfaceMotion(dpn, target, active);
                break;
        }
    }

    private void updateMovementVelocity(DynamicPhysicsNode dpn, Vector3f target, boolean active) {
        if (active == false) {
            return;
        }
        Vector3f diff = target.subtract(dpn.getLocalTranslation());
        diff.normalizeLocal().multLocal(10);
        diff.setY(dpn.getLinearVelocity(null).getY());
        dpn.setLinearVelocity(diff);
    }

    private void updateMovementForce(DynamicPhysicsNode dpn, Vector3f target, boolean active) {
        if (active == false) {
            dpn.addForce(new Vector3f(0, -10, 0));
            return;
        }
        Vector3f diff = target.subtract(dpn.getLocalTranslation());
        diff.normalizeLocal().multLocal(200);
        diff.y = -10;
        dpn.addForce(diff);
    }

    private void updateMovementSurfaceMotion(DynamicPhysicsNode dpn, Vector3f target, boolean active) {
        if (active == false) {
            dpn.getMaterial().setSurfaceMotion(Vector3f.ZERO);
            return;
        }
        Vector3f diff = target.subtract(dpn.getLocalTranslation());
        diff.normalizeLocal().multLocal(10);
        dpn.getMaterial().setSurfaceMotion(diff);
    }

    private void updateTarget() {
        if (MouseInput.get().isButtonDown(0) == false) {
            return;
        }

        Vector2f screenPos = new Vector2f(MouseInput.get().getXAbsolute(), MouseInput.get().getYAbsolute());
        Vector3f worldCoords = display.getWorldCoordinates(screenPos, 0);
        Vector3f worldCoords2 = display.getWorldCoordinates(screenPos, 1);
        Ray mouseRay = new Ray(worldCoords, worldCoords2.subtractLocal(worldCoords).normalizeLocal());

        PickResults pickResults = new BoundingPickResults();
        pickResults.setCheckDistance(true);
        terrainNode.findPick(mouseRay, pickResults);
        for (int i = 0; i < pickResults.getNumber(); i++) {
            BresenhamTerrainPicker terrainPicker = new BresenhamTerrainPicker(pickResults.getPickData(i).getTargetMesh());
            Vector3f res = terrainPicker.getTerrainIntersection(mouseRay, null);
            if (res != null) {
                targetloc.set(res);
                break;
            }
            //TODO find intersections with other elements other than TerrainBlocks
        }
    }

    public static void main(String[] args) {
        Logger.getLogger("").setLevel(Level.WARNING); // to see the important stuff
        new YawnPhysics().start();
    }

    public enum MovementStyle {

        VELOCITY,
        FORCE,
        SURFACEMOTION,
    }
}

i didnt test it yet, but did you try with different predefined Materials to reduce the 'bounciness'?



//dpn.setMaterial(new Material());
dpn.setMaterial(Material.GRANITE);

Core-Dump said:

i didnt test it yet, but did you try with different predefined Materials to reduce the 'bounciness'?


//dpn.setMaterial(new Material());
dpn.setMaterial(Material.GRANITE);




I retested it, but it doesn't seem to matter much (I didn't spot a big difference), but maybe that is because I'm still doing something fundamentally wrong.

Hi, Grif.


- When I move an object over the terrain, it shouldn't get 'airborne' when it hits a bump (just as a human doesn't get airborne when he walks over a small bump).


I had the same problem with my controllable ball. I multiplied its mass with 100 to keep him under control. Also the frictionCallback does a lot. If you need some code, just ask, I just programmed a bit on that.

- when 2 objects collide, the object that was standing still (that is a DynamicPhysicsNode but just isn't moving at the time) should slightly move aside to let the other object pass. Block the moving object altogether is valid as well, as long as the moving object doesn't 'drag' the still object away. Would be great if it could be specified per object


You can "select" the two nodes that participate at a collision with a CollisionCallback f.e. It's pretty simple, here is a very small example of what I did recently:


        ContactCallback playerBlockCallback = new ContactCallback() {

         @Override
         public boolean adjustContact(PendingContact contact) {
            
            if (contact.getNode1().getName().equals("player") &&
                  contact.getNode2().getName().equals("player")) {
               
               contact.setIgnored(true);
            
               return true;
            }
            
            return false;
         }
           
        };
       
        getPhysicsSpace().getContactCallbacks().add(playerBlockCallback);



In this, the collision between two player-elements just is ignored.


- moving object doesn't get airborne when it collides with a still object.


And you can use a ContactCallback here, too. Also watch out you don't change translations/rotations/scale by setting the values but add Forces/Torques etc. all the time! Because the direct setting of values causes bumpy behaviour, too.

My CharacterNode and CharacterController classes fix the airbone when you pass tru a bump. tough they won't walk proprially because i can't spin the ball(feet) proprially yet…

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package Character;

import com.jme.input.FirstPersonHandler;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.scene.Controller;
import com.jmex.physics.PhysicsSpace;

/**
 *
 * @author Guedez
 */
public class CharacterController extends Controller {

    private CharacterNode character;
    private KeyBindingManager keyboard = KeyBindingManager.getKeyBindingManager();
    private Vector3f speed = new Vector3f(0, 0, 0);
    private Vector3f jspeed = new Vector3f(0, 0, 0);
    private Camera cam;
    private FirstPersonHandler input;
    private byte onGround;
    private PhysicsSpace space;
    private byte jumping;

    public CharacterController(CharacterNode character, Camera cam,
            PhysicsSpace space, float turnspeed) {
        input = new FirstPersonHandler(cam, 0, turnspeed);
        this.cam = cam;
        //this.cam.setLocation(character.getCylinder().getLocalTranslation());
        this.character = character;
        onGround = 0;
        this.space = space;
        keyboard.add("MoveFowardBall", KeyInput.KEY_W);
        keyboard.add("StrifeLeftBall", KeyInput.KEY_Q);
        keyboard.add("StrifeRightBall", KeyInput.KEY_E);
        keyboard.add("TurnLeftBall", KeyInput.KEY_A);
        keyboard.add("TurnRightBall", KeyInput.KEY_D);
        keyboard.add("MoveBackwardBall", KeyInput.KEY_S);
        keyboard.add("MoveRunBall", KeyInput.KEY_LSHIFT);
        keyboard.add("MoveJumpBall", KeyInput.KEY_SPACE);
        input.getKeyboardLookHandler().clearActions();
    }

    @Override
    public void update(float tpf) {
        input.update(tpf);
        cam.update();
        speed.x = 0;
        speed.y = 0;//character.getSphere().getLinearVelocity(null).y;
        speed.z = 0;
        Vector3f temp;
        if (keyboard.isValidCommand("MoveJumpBall", false)) {
            if (onGround > 0) {
                jspeed = character.getCylinder().getLinearVelocity(null);
                jspeed.y = 150;
                character.getSphere().setLinearVelocity(jspeed);
                character.getCylinder().setLinearVelocity(jspeed);
                jumping = 15;
            }
        }
        if (keyboard.isValidCommand("MoveRunBall")) {
            temp = character.getCylinder().getLocalRotation().
                    getRotationColumn(2).mult(25f);
            speed.y = temp.y;
        } else {
            //temp = cam.getLeft().mult(2.5f);
            temp = character.getCylinder().getLocalRotation().
                    getRotationColumn(2).mult(12.5f);
            speed.y = temp.y;
        }
        if (keyboard.isValidCommand("MoveFowardBall") &&
                !keyboard.isValidCommand("MoveBackwardBall")) {
            speed.x -= temp.x;
            speed.z -= temp.z;

        }
        if (keyboard.isValidCommand("MoveBackwardBall") &&
                !keyboard.isValidCommand("MoveFowardBall")) {
            speed.x += temp.x/2;
            speed.z += temp.z/2;
        }
        if (keyboard.isValidCommand("StrifeLeftBall")) {
            speed.x -= temp.z;
            speed.z += temp.x;
        }
        if (keyboard.isValidCommand("StrifeRightBall")) {
            speed.x -= temp.z;
            speed.z -= temp.x;
        }
        if (keyboard.isValidCommand("TurnLeftBall")) {
            character.setNewspin(40);
           
        }
        if (keyboard.isValidCommand("TurnRightBall")) {
            character.setNewspin(-40);
        }

        if ((onGround == 0 || onGround == -1) && jumping == -1) {
            speed.y = 0;
            onGround--;
            System.out.println("Y = 0");
        }
        if (jumping > 0) {
            System.out.println(jumping);
            jumping--;
        } else if (onGround > 0) {
            System.out.println(onGround);
            onGround--;
        }
        character.getSphere().setAngularVelocity(speed.divide(5));
        //character.getCylinder().setLinearVelocity(speed);
    }

    public byte getOnGround() {
        return onGround;
    }

    public byte getJumping() {
        return jumping;
    }

    public void setJumping(int jumping) {
        this.jumping = (byte) jumping;
    }

    public void setOnGround(int onGround) {
        this.onGround = (byte) onGround;
    }
}


/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package Character;

import com.jme.bounding.BoundingBox;
import com.jme.input.InputHandler;
import com.jme.input.action.InputAction;
import com.jme.input.action.InputActionEvent;
import com.jme.input.util.SyntheticButton;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jmex.physics.DynamicPhysicsNode;
import com.jmex.physics.Joint;
import com.jmex.physics.JointAxis;
import com.jmex.physics.PhysicsSpace;
import com.jmex.physics.RotationalJointAxis;
import com.jmex.physics.contact.ContactInfo;
import com.jmex.physics.geometry.PhysicsCylinder;
import com.jmex.physics.geometry.PhysicsSphere;

/**
 *
 * @author Guedez
 */
public class CharacterNode extends Node {

    private DynamicPhysicsNode sphere;
    private Joint joint;
    private DynamicPhysicsNode cylinder;
    private Spatial model;
    private PhysicsSpace space;
    private Node head = new Node();
    private InputHandler contactDetect;
    private float newspin = 0;
    private boolean allowFall = false;
    RotationalJointAxis feetWalkAxis;
    //private byte jumping;
    private CharacterController controller;

    public CharacterNode(Spatial model, PhysicsSpace space, Camera cam, float turnSpeed) {
        super();
        this.model = model;
        this.model.setModelBound(new BoundingBox());
        this.model.updateModelBound();
        this.model.updateWorldBound();
        this.space = space;
        //Creating the FootSphere and the BodyCylinder
        cylinder = space.createDynamicNode();
        sphere = space.createDynamicNode();
        PhysicsCylinder c = cylinder.createCylinder("Body");
        cylinder.attachChild(c);
        PhysicsSphere s = cylinder.createSphere("Body");
        sphere.attachChild(s);
        //Defining it's sizes by the model size, i have no idea how this
        //calculation works, it just does
        //Initiates the Sphere
        BoundingBox bbox = (BoundingBox) model.getWorldBound();
        float cRadius = bbox.xExtent > bbox.zExtent ? bbox.zExtent : bbox.xExtent;
        float cHeight = bbox.yExtent * 2f;
        s.setLocalScale(cRadius);
        sphere.setMaterial(CharacterMaterial.getInstance());
        sphere.computeMass();
        model.setLocalTranslation(0, (cHeight / 2) - cRadius, 0);
        //Initiates the Cylinder
        c.setLocalScale(new Vector3f(cRadius, cRadius, cHeight - cRadius));
        c.setLocalTranslation(0, (cHeight - cRadius) / 2, 0);
        Quaternion rot = new Quaternion();
        rot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
        c.setLocalRotation(rot);
        cylinder.computeMass();
        //Attaches stuff to itself
        this.attachChild(model);
        this.attachChild(sphere);
        this.attachChild(cylinder);
        // create a joint to keep the capsule locked to the rolling sphere
        joint = space.createJoint();
        JointAxis axis = joint.createRotationalAxis();
        axis.setDirection(Vector3f.UNIT_X);
        joint.attach(cylinder, sphere);
        feetWalkAxis = joint.createRotationalAxis();
        feetWalkAxis.setDirection(new Vector3f(0, 0, 1));
        feetWalkAxis.setAvailableAcceleration(10);
        feetWalkAxis.setRelativeToSecondObject(true);
        //Defines if the character is or not on the ground
        SyntheticButton collButton = sphere.getCollisionEventHandler();
        contactDetect = new InputHandler();
        contactDetect.addAction(new InputAction() {

            @Override
            public void performAction(InputActionEvent evt) {
                ContactInfo contactInfo = (ContactInfo) evt.getTriggerData();
                Vector3f vec = contactInfo.getContactNormal(null);
                float dot = vec.dot(Vector3f.UNIT_Y);
                if (dot > -1f && dot < -0.75f) {
                    System.out.println(dot);
                    if (!(controller.getJumping() > 0)) {
                        controller.setOnGround(3);
                        controller.setJumping(-1);
                    }
                }
                Vector3f vel = contactInfo.getContactVelocity(null);
                if (vel.length() > 10) {
                    System.out.println("POWERFUL CONTACT: " + vel.length());
                }
            }
        }, collButton, false);
        controller = new CharacterController(this, cam, space, turnSpeed);
        cylinder.attachChild(head);
        head.setLocalTranslation(0, cHeight, 0);
        cam.setLocation(head.getLocalTranslation());
        cylinder.attachChild(model);
    }

    public void preventFall(float interpolation) {
        Quaternion q = cylinder.getLocalRotation();
        Vector3f[] axes = new Vector3f[3];
        q.toAxes(axes);
        q.fromAxes(axes[0], Vector3f.UNIT_Y, axes[2]);
        cylinder.setLocalRotation(q);
        cylinder.setAngularVelocity(Vector3f.ZERO.add(0, newspin*interpolation, 0));
        newspin = 0;
        cylinder.updateWorldVectors();
    }

    public void update(float interpolation) {
        controller.update(interpolation);
        //resetFeetRotation();
        if (!allowFall) {
            preventFall(interpolation);
        }
        contactDetect.update(interpolation);
    //onGround = controller.getOnGround();
    //jumping = controller.getJumping();
    }

    public DynamicPhysicsNode getSphere() {
        return sphere;
    }

    public DynamicPhysicsNode getCylinder() {
        return cylinder;
    }

    public void setNewspin(float newspin) {
        this.newspin = newspin;
    }

    public void setAllowFall(boolean allowFall) {
        this.allowFall = allowFall;
    }

    private void resetFeetRotation(){
        Quaternion q = sphere.getLocalRotation();
        Vector3f[] axes = new Vector3f[3];
        q.toAxes(axes);

        q.fromAxes(Vector3f.UNIT_X, Vector3f.UNIT_Y, Vector3f.UNIT_Z);
        sphere.setLocalRotation(new Quaternion());
    }

    public RotationalJointAxis getFeetWalkAxis() {
        return feetWalkAxis;
    }
}



also, they used to use the camera position for movement, there is a lot of trash in the class yet since it is still under construction ;D