Another Character Control

Hey monkeys, this is what I’ve been working on all week and I thought I’d share it: a modified version of Normen’s BetterCharacterControl…
Features:
-all of the normal ones
-checks to see if something is in front of it so it doesn’t get stuck in walls
-sweep test to check to see if it’s on ground
-tons of variables that you can tweak and such
-has option to increase speed when running
-has option to disable in-air movement
-depending on the constructor, it can rotate the character model towards acceleration

Vid (note that I pumped up all the speed values and such so it would be obvious):
[video]http://www.youtube.com/watch?v=QLfiRx6GaVI[/video]

Control:
[java]package mygame.controls;

import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.PhysicsTickListener;
import com.jme3.bullet.collision.PhysicsRayTestResult;
import com.jme3.bullet.collision.PhysicsSweepTestResult;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.control.AbstractPhysicsControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.collision.CollisionResults;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
import com.jme3.util.TempVars;
import java.io.IOException;
import java.util.List;

public class NewCharacterControl extends AbstractPhysicsControl implements PhysicsTickListener {

protected PhysicsRigidBody rigidBody;
protected float radius;
protected float height;
protected float mass;
protected float duckedFactor = 0.6f;
//for rotational purposes
protected Spatial node, rotModel;
protected Node rootNode;
protected Node mainNode;
protected final Vector3f localUp = new Vector3f(0, 1, 0);
protected final Vector3f localForward = new Vector3f(0, 0, 1);
protected final Vector3f localLeft = new Vector3f(1, 0, 0);
protected final Quaternion localForwardRotation = new Quaternion(Quaternion.DIRECTION_Z);
protected final Vector3f viewDirection = new Vector3f(0, 0, 1);
protected final Vector3f location = new Vector3f();


protected final Quaternion rotation = new Quaternion(Quaternion.DIRECTION_Z);
protected final Vector3f rotatedViewDirection = new Vector3f(0, 0, 1);
protected final Vector3f walkDirection = new Vector3f();
protected final Vector3f jumpDirection= new Vector3f();
protected final Vector3f jumpForce;
protected float physicsDamping = 0.5f;
protected final Vector3f scale = new Vector3f(1, 1, 1);
protected final Vector3f velocity = new Vector3f();
//not necessarily height; just rate increase
protected float jumpHeight=1;
protected float minSpeed=4;
protected float speed=minSpeed;
protected float maxSpeed=16;
//speed increase rate
protected float speedConstant=2f;
protected boolean jump = false;

//if you want character to move during jump
protected boolean moreControl=true;
//if you want speed to increase as they run
protected boolean speedIncrease = false;

protected boolean onGround = false;
protected boolean somethingInFront = false;
protected boolean ducked = false;
protected boolean wantToUnDuck = false;
protected boolean leftStrafe = false, rightStrafe = false, forward = false, backward = false,
        leftRotate = false, rightRotate = false;
//so you can't do 50million jumps right after eachother
protected float maxJumpTime=0.3f;
protected float jumpWaitTime=maxJumpTime;
//could be used for flailing animation?
protected float airTime=0;
//height off the base of the char for wall testing
protected float stepHeight=1.5f;

//for checking on ground
protected CapsuleCollisionShape sweepShape;


public NewCharacterControl() {
    jumpForce = new Vector3f();
}
//for no rotational char
public NewCharacterControl(float radius, float height, float mass, Spatial m) {
    this.radius = radius;
    this.height = height;
    this.mass = mass;
    rigidBody = new PhysicsRigidBody(getShape(), mass);
    jumpForce = new Vector3f(0, mass * jumpHeight*speed, 0);
    sweepShape = new CapsuleCollisionShape(getFinalRadius()*0.95f,0.05f);
    rigidBody.setAngularFactor(0);
    rigidBody.setRestitution(0);
    node=m;
    
}
//for rotational char
public NewCharacterControl(Node a,float radius, float height, float mass, Spatial m, Spatial rot) {
    this.radius = radius;
    this.height = height;
    this.mass = mass;
    rigidBody = new PhysicsRigidBody(getShape(), mass);
    jumpForce = new Vector3f(0, mass * jumpHeight*speed, 0);
    sweepShape = new CapsuleCollisionShape(getFinalRadius()*0.95f,0.05f);
    rigidBody.setAngularFactor(0);
    rigidBody.setRestitution(0);
    node=m;
    rotModel=rot;
    rootNode=a;
    mainNode=new Node();
    mainNode.attachChild(rotModel);
    rootNode.attachChild(mainNode);
    
}

@Override
public void update(float tpf) {
    super.update(tpf);
    rigidBody.getPhysicsLocation(location);
    //update force with speed if you have that set to change
    if(speedIncrease){
        setJumpForce(new Vector3f(0, mass * jumpHeight * speed, 0));
    }
    
    applyPhysicsTransform(location, rotation);
    
    if(rotModel!=null){
        rotModel.setLocalRotation(getLocalRotation());
        mainNode.setLocalTranslation(getPhysicsLocation());
        Vector3f vv = getVelocity().cross(getLocalUp());
        Quaternion q = new Quaternion();
        q.fromAngleAxis(getVelocity().length()*0.03f, vv.negate());
        q.nlerp(mainNode.getLocalRotation(),0.8f);

        final Quaternion j = new Quaternion();
        j.set(mainNode.getLocalRotation());
        j.slerp(q, tpf*20);
        mainNode.setLocalRotation(j);
    }
    
}

@Override
public void render(RenderManager rm, ViewPort vp) {
    super.render(rm, vp);
}

public void prePhysicsTick(PhysicsSpace space, float tpf) {
    checkOnGround();
    
    
    
    if (wantToUnDuck && checkCanUnDuck()) {
        setHeightPercent(1);
        wantToUnDuck = false;
        ducked = false;
    }
    TempVars vars = TempVars.get();

    // dampen existing x/z forces
    float existingLeftVelocity = velocity.dot(localLeft);
    float existingForwardVelocity = velocity.dot(localForward);
    Vector3f counter = vars.vect1;
    existingLeftVelocity = existingLeftVelocity * physicsDamping;
    existingForwardVelocity = existingForwardVelocity * physicsDamping;
    counter.set(-existingLeftVelocity, 0, -existingForwardVelocity);
    localForwardRotation.multLocal(counter);
    velocity.addLocal(counter);

    float designatedVelocity = walkDirection.length();
    if (designatedVelocity > 0) {
        Vector3f localWalkDirection = vars.vect1;
        //normalize walkdirection
        localWalkDirection.set(walkDirection).normalizeLocal();
        //check for the existing velocity in the desired direction
        float existingVelocity = velocity.dot(localWalkDirection);
        //calculate the final velocity in the desired direction
        float finalVelocity = designatedVelocity - existingVelocity;
        localWalkDirection.multLocal(finalVelocity);
        //add resulting vector to existing velocity
        velocity.addLocal(localWalkDirection);
    }
    checkSomethingInFront();
    if(!somethingInFront){
        rigidBody.setLinearVelocity(velocity);
    }
    
    
    
    if (jump) {
        //TODO: precalculate jump force
        Vector3f rotatedJumpForce = vars.vect1;
        rotatedJumpForce.set(jumpForce);
        rigidBody.applyImpulse(localForwardRotation.multLocal(rotatedJumpForce), Vector3f.ZERO);
        jumpWaitTime+=tpf;
        jump = false;
    }
    vars.release();
}

public void physicsTick(PhysicsSpace space, float tpf) {
    rigidBody.getLinearVelocity(velocity);
    
    Vector3f modelForwardDir = node.getWorldRotation().mult(Vector3f.UNIT_Z);
    Vector3f modelLeftDir = node.getWorldRotation().mult(Vector3f.UNIT_X);
    
    walkDirection.set(0, 0, 0);
    if(moreControl){
        if (leftStrafe) {
            walkDirection.addLocal(modelLeftDir);
        } else if (rightStrafe) {
            walkDirection.addLocal(modelLeftDir.negate());
        }
        if (forward) {
            walkDirection.addLocal(modelForwardDir);
        } else if (backward) {
            walkDirection.addLocal(modelForwardDir.negate());
        }
        
        
        if(!walkDirection.equals(Vector3f.ZERO)&&speedIncrease){
            if(speed<maxSpeed){
                speed+=speedConstant*tpf;
            }else{
                speed=maxSpeed;
            }
        }else{
            if(speed>minSpeed){
                speed-=speedConstant*tpf*4;
            }else{
                speed=minSpeed;
            }
            
        }
    }else{
        if(onGround){      
            if (leftStrafe) {
                walkDirection.addLocal(modelLeftDir);
            } else if (rightStrafe) {
                walkDirection.addLocal(modelLeftDir.negate());
            }
            if (forward) {
                walkDirection.addLocal(modelForwardDir);
            } else if (backward) {
                walkDirection.addLocal(modelForwardDir.negate());
            }


            
            if(!walkDirection.equals(Vector3f.ZERO)&&speedIncrease){
                if(speed<maxSpeed){
                    speed+=speedConstant*tpf;
                }else{
                    speed=maxSpeed;
                }
            }else{
                if(speed>minSpeed){
                    speed-=speedConstant*tpf*4;
                }else{
                    speed=minSpeed;
                }

            }
            
        }else{
          walkDirection.set(jumpDirection.normalize());  
        }
        
        
    }
    walkDirection.multLocal(speed);
    jumpDirection.set(walkDirection);
    
    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);
    }
   setViewDirection(viewDirection);
   if(jumpWaitTime>0&&jumpWaitTime<maxJumpTime){
       jumpWaitTime+=tpf;
   }
   if(onGround){
       airTime=0;
   }else{
       airTime+=tpf;
   }
}

public void warp(Vector3f vec) {
    setPhysicsLocation(vec);
}

public void jump() {
    
    if (!onGround) {
        return;
    }
    
    if(jumpWaitTime>=maxJumpTime){
        
        jumpWaitTime=0;
        jump = true;
        
    }
    
}

public void setJumpForce(Vector3f jumpForce) {
    this.jumpForce.set(jumpForce);
}

public Vector3f getJumpForce() {
    return jumpForce;
}

public boolean isOnGround() {
    return onGround;
}

public void setDucked(boolean enabled) {
    if (enabled) {
        setHeightPercent(duckedFactor);
        ducked = true;
        wantToUnDuck = false;
    } else {
        if (checkCanUnDuck()) {
            setHeightPercent(1);
            ducked = false;
        } else {
            wantToUnDuck = true;
        }
    }
}

public boolean isDucked() {
    return ducked;
}

public void setDuckedFactor(float factor) {
    duckedFactor = factor;
}

public float getDuckedFactor() {
    return duckedFactor;
}

public void setWalkDirection(Vector3f vec) {
    walkDirection.set(vec);
}

public Vector3f getWalkDirection() {
    return walkDirection;
}

public void setViewDirection(Vector3f vec) {
    viewDirection.set(vec);
    updateLocalViewDirection();
}

public Vector3f getViewDirection() {
    return viewDirection;
}

public void resetForward(Vector3f vec) {
    if (vec == null) {
        vec = Vector3f.UNIT_Z;
    }
    localForward.set(vec);
    updateLocalCoordinateSystem();
}

public Vector3f getVelocity() {
    return velocity;
}

public void setGravity(Vector3f gravity) {
    rigidBody.setGravity(gravity);
    localUp.set(gravity).normalizeLocal().negateLocal();
    updateLocalCoordinateSystem();
}

public Vector3f getGravity() {
    return rigidBody.getGravity();
}

public Vector3f getGravity(Vector3f store) {
    return rigidBody.getGravity(store);
}

public void setPhysicsDamping(float physicsDamping) {
    this.physicsDamping = physicsDamping;
}

public float getPhysicsDamping() {
    return physicsDamping;
}

protected void setHeightPercent(float percent) {
    scale.setY(percent);
    rigidBody.setCollisionShape(getShape());
}

//sweep test of the capsule shape to see if it's on the ground
protected void checkOnGround() {
    TempVars vars = TempVars.get();
    Vector3f loc = vars.vect1;
    Vector3f rayVector = vars.vect2;
    loc.set(location).addLocal(localUp);
    rayVector.set(location).subtractLocal(new Vector3f(0,0.2f,0));
    List<PhysicsSweepTestResult> results = space.sweepTest(getSweepShape(), new Transform(loc), new Transform(rayVector)); 
    vars.release();
    
    for (PhysicsSweepTestResult physicsSweepTestResult : results) {
        if (!physicsSweepTestResult.getCollisionObject().equals(rigidBody)&&physicsSweepTestResult.getCollisionObject()instanceof RigidBodyControl) {
            onGround = true;
            return;
        }
    }
    
    onGround = false;
}

//Check to see if something above
protected boolean checkCanUnDuck() {
    TempVars vars = TempVars.get();
    Vector3f loc = vars.vect1;
    Vector3f rayVector = vars.vect2;
    loc.set(localUp).multLocal(FastMath.ZERO_TOLERANCE).addLocal(this.location);
    rayVector.set(localUp).multLocal(height + FastMath.ZERO_TOLERANCE).addLocal(loc);
    List<PhysicsRayTestResult> results = space.rayTest(loc, rayVector);
    vars.release();
    for (PhysicsRayTestResult physicsRayTestResult : results) {
        if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)) {
            return false;
        }
    }
    return true;
}

//if there's something in front return true
protected void checkSomethingInFront() {
    TempVars vars = TempVars.get();
    Vector3f loc = vars.vect1;
    Vector3f rayVector = vars.vect2;
    Vector3f velNorm = vars.vect3;
    loc.set(location.add(0, height, 0));
    velNorm.set(velocity.normalize());
   rayVector.set(new Vector3f(location.getX(),location.getY()-(height/2)+stepHeight,location.getZ()));
   rayVector.addLocal(velNorm);
    List<PhysicsRayTestResult> results = space.rayTest(loc, rayVector);
    vars.release();
    for (PhysicsRayTestResult physicsRayTestResult : results) {
        if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)&&physicsRayTestResult.getCollisionObject()instanceof RigidBodyControl) {
             somethingInFront = true;
            return;
        }
    }
    TempVars vars2 = TempVars.get();
    loc = vars2.vect1;
    rayVector = vars2.vect2;
    velNorm = vars2.vect3;
    
    loc.set(new Vector3f(location.getX(),location.getY()-(height/2)+stepHeight,location.getZ()));
    velNorm.set(velocity.normalize());
    rayVector.set(location.add(0, height, 0));
    rayVector.addLocal(velNorm);
    results = space.rayTest(loc, rayVector);
    vars2.release();
    for (PhysicsRayTestResult physicsRayTestResult : results) {
        if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)&&physicsRayTestResult.getCollisionObject()instanceof RigidBodyControl) {
           somethingInFront = true;
            return;
        }
    }
    somethingInFront = false;
}

protected CollisionShape getShape() {
    CapsuleCollisionShape capsuleCollisionShape = new CapsuleCollisionShape(getFinalRadius(), (getFinalHeight() - (2 * getFinalRadius())));
    CompoundCollisionShape compoundCollisionShape = new CompoundCollisionShape();
    Vector3f addLocation = new Vector3f(0, (getFinalHeight() / 2.0f), 0);
    compoundCollisionShape.addChildShape(capsuleCollisionShape, addLocation);
    return compoundCollisionShape;
}
protected CapsuleCollisionShape getSweepShape() {
    return sweepShape;
}

protected float getFinalHeight() {
    return height * scale.getY();
}

protected float getFinalRadius() {
    return radius * scale.getZ();
}

protected void updateLocalCoordinateSystem() {
    //gravity vector has possibly changed, calculate new world forward (UNIT_Z)
    calculateNewForward(localForwardRotation, localForward, localUp);
    localLeft.set(localUp).crossLocal(localForward);
    rigidBody.setPhysicsRotation(localForwardRotation);
    updateLocalViewDirection();
}

protected void updateLocalViewDirection() {
    //update local rotation quaternion to use for view rotation
    localForwardRotation.multLocal(rotatedViewDirection.set(viewDirection));
    calculateNewForward(rotation, rotatedViewDirection, localUp);
}

protected final void calculateNewForward(Quaternion rotation, Vector3f direction, Vector3f worldUpVector) {
    if (direction == null) {
        return;
    }
    TempVars vars = TempVars.get();
    Vector3f newLeft = vars.vect1;
    Vector3f newLeftNegate = vars.vect2;

    newLeft.set(worldUpVector).crossLocal(direction).normalizeLocal();
    if (newLeft.equals(Vector3f.ZERO)) {
        if (direction.x != 0) {
            newLeft.set(direction.y, -direction.x, 0f).normalizeLocal();
        } else {
            newLeft.set(0f, direction.z, -direction.y).normalizeLocal();
        }
    }
    newLeftNegate.set(newLeft).negateLocal();
    direction.set(worldUpVector).crossLocal(newLeftNegate).normalizeLocal();
    if (direction.equals(Vector3f.ZERO)) {
        direction.set(Vector3f.UNIT_Z);
    }
    if (rotation != null) {
        rotation.fromAxes(newLeft, worldUpVector, direction);
    }
    vars.release();
}

@Override
protected void setPhysicsLocation(Vector3f vec) {
    rigidBody.setPhysicsLocation(vec);
    location.set(vec);
}

@Override
protected void setPhysicsRotation(Quaternion quat) {
    rotation.set(quat);
    rotation.multLocal(rotatedViewDirection.set(viewDirection));
    updateLocalViewDirection();
}

@Override
protected void addPhysics(PhysicsSpace space) {
    space.getGravity(localUp).normalizeLocal().negateLocal();
    updateLocalCoordinateSystem();

    space.addCollisionObject(rigidBody);
    space.addTickListener(this);
}

@Override
protected void removePhysics(PhysicsSpace space) {
    space.removeCollisionObject(rigidBody);
    space.removeTickListener(this);
}

@Override
protected void createSpatialData(Spatial spat) {
    rigidBody.setUserObject(spatial);
}

@Override
protected void removeSpatialData(Spatial spat) {
    rigidBody.setUserObject(null);
}

public Control cloneForSpatial(Spatial spatial) {
    NewCharacterControl control = new NewCharacterControl(radius, height, mass, node);
    control.setJumpForce(jumpForce);
    return control;
}

@Override
public void write(JmeExporter ex) throws IOException {
    super.write(ex);
    OutputCapsule oc = ex.getCapsule(this);
    oc.write(radius, "radius", 1);
    oc.write(height, "height", 1);
    oc.write(mass, "mass", 1);
    oc.write(jumpForce, "jumpForce", new Vector3f(0, mass * 5, 0));
    oc.write(physicsDamping, "physicsDamping", 0.9f);
}

@Override
public void read(JmeImporter im) throws IOException {
    super.read(im);
    InputCapsule in = im.getCapsule(this);
    this.radius = in.readFloat("radius", 1);
    this.height = in.readFloat("height", 2);
    this.mass = in.readFloat("mass", 80);
    this.physicsDamping = in.readFloat("physicsDamping", 0.9f);
    this.jumpForce.set((Vector3f) in.readSavable("jumpForce", new Vector3f(0, mass * 5, 0)));
    rigidBody = new PhysicsRigidBody(getShape(), mass);
    jumpForce.set(new Vector3f(0, mass * 5, 0));
    rigidBody.setAngularFactor(0);
}

public void setLeftStrafe(boolean leftStrafe) {
    this.leftStrafe = leftStrafe;
}
public void setRightStrafe(boolean rightStrafe) {
    this.rightStrafe = rightStrafe;
}

public void setForward(boolean forward) {
    this.forward = forward;
}

public void setBackward(boolean backward) {
    this.backward = backward;
}

public void setLeftRotate(boolean leftRotate) {
    this.leftRotate = leftRotate;
}

public void setRightRotate(boolean rightRotate) {
    this.rightRotate = rightRotate;
}
public Vector3f getPhysicsLocation(){
    return rigidBody.getPhysicsLocation();
}

public void setJumpHeight(float height){
    jumpHeight=height;
    setJumpForce(new Vector3f(0, mass * jumpHeight*speed, 0));
}

public Vector3f getLocalUp() {
    return localUp;
}
public Quaternion getLocalRotation(){
    return node.getLocalRotation();
}
public float getSpeed(){
    return speed;
}

public Vector3f getJumpDirection() {
    return jumpDirection;
}

public float getRadius() {
    return radius;
}

public void setRadius(float radius) {
    this.radius = radius;
}

public float getMass() {
    return mass;
}

public void setMass(float mass) {
    this.mass = mass;
}

public float getMinSpeed() {
    return minSpeed;
}

public void setMinSpeed(float minSpeed) {
    this.minSpeed = minSpeed;
}

public float getMaxSpeed() {
    return maxSpeed;
}

public void setMaxSpeed(float maxSpeed) {
    this.maxSpeed = maxSpeed;
}

public float getSpeedConstant() {
    return speedConstant;
}

public void setSpeedConstant(float speedConstant) {
    this.speedConstant = speedConstant;
}

public boolean isMoreControl() {
    return moreControl;
}

public void setMoreControl(boolean moreControl) {
    this.moreControl = moreControl;
}

public boolean isSpeedIncrease() {
    return speedIncrease;
}

public void setSpeedIncrease(boolean speedIncrease) {
    this.speedIncrease = speedIncrease;
}

public float getMaxJumpTime() {
    return maxJumpTime;
}

public void setMaxJumpTime(float maxJumpTime) {
    this.maxJumpTime = maxJumpTime;
}

public float getJumpWaitTime() {
    return jumpWaitTime;
}

public void setJumpWaitTime(float jumpWaitTime) {
    this.jumpWaitTime = jumpWaitTime;
}

public float getStepHeight() {
    return stepHeight;
}

public void setStepHeight(float stepHeight) {
    this.stepHeight = stepHeight;
}

public float getHeight() {
    return height;
}

}
[/java]

Usage (for character rotation):
[java] characterNode = new Node(“character node”);

    model = (Node) getAssetManager().loadModel("Models/YoCharModel.j3o");
    characterNode.attachChild(model);
    
    physicsCharacter = new NewCharacterControl(rootNode, 0.5f, 2f, 8f, characterNode, model);
    characterNode.addControl(physicsCharacter);
    
    rootNode.attachChild(characterNode);
    getPhysicsSpace().add(physicsCharacter);[/java]

Usage (without character rotation):
[java] characterNode = new Node(“character node”);

    physicsCharacter = new NewCharacterControl(0.5f, 2f, 8f, characterNode);
    characterNode.addControl(physicsCharacter);
    
    rootNode.attachChild(characterNode);
    getPhysicsSpace().add(physicsCharacter);[/java]

Then you can enable/disable speed increase and air mobility:
[java] physicsCharacter.setMoreControl(true);
physicsCharacter.setSpeedIncrease(true);[/java]
And can edit all the different variables by using all the different functions in there…

So yeah, if you like it, cool, if it’s inefficient, please tell me how to make it better, if you make it better, I would love to learn! (it’s probably riddled with bugs and bad coding styles, sorry; first time using the physics engine)

-NomNom

3 Likes

Nice work, i like the rotation part and the speed , but yeah theres some bugs,when i jump and press the W button dont stuck you in the air, but when you press W in air and then try to move to right or left it hold to the wall when fall slowly and you can move fast to the right or left, i dont know if it happens only to me, and well the other thing its the object bounce when it tries to pass the wall, but that is a problem from the old character controls.