BetterCharacterControl in the works

I also have a question related to the BCC.

I recall a way to set a maximum slope but if I remember correctly it was removed.

Does anyone know a simple way to set a maximum slope that the character can travel up.

Well you could use this, I wouldn’t exactly call it clean code, but it works.
Then you can check against the totalAngle, and scale move vector or whatever with it.

[java]
final Vector3f localUp = new Vector3f(0, 1, 0);
controll.getRotation().multLocal(localUp);
final float totalAngle = FastMath.acos(controll.getNormal().dot(localUp)) * FastMath.RAD_TO_DEG;
[/java]

[java]
package de.visiongamestudios.humanoid;

import java.util.List;

import com.jme3.bullet.collision.PhysicsRayTestResult;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.math.Vector3f;
import com.jme3.util.TempVars;

public class VGSBetterCharacterControll extends BetterCharacterControl {
private Vector3f normal = new Vector3f(0, 1, 0);

public VGSBetterCharacterControll(final float f, final float size, final int mass) {
	super(f, size, mass);
}

public Vector3f getNormal() {
	return this.normal;
}

/**
 * This checks if the character is on the ground by doing a ray test.
 */
@Override
protected void checkOnGround() {
	final TempVars vars = TempVars.get();
	final Vector3f location = vars.vect1;
	final Vector3f rayVector = vars.vect2;
	final float height = this.getFinalHeight();

	location.set(this.localUp).multLocal(height).addLocal(this.location);
	rayVector.set(this.localUp).multLocal(-50).addLocal(location);
	final int length = 52;
	final List<PhysicsRayTestResult> results = this.space.rayTest(location, rayVector);
	vars.release();
	this.onGround = false;
	float closest = Float.MAX_VALUE;
	for (final PhysicsRayTestResult physicsRayTestResult : results) {
		if (!physicsRayTestResult.getCollisionObject().equals(this.rigidBody)) {
			if (closest > physicsRayTestResult.getHitFraction()) {
				closest = physicsRayTestResult.getHitFraction();
				this.normal = physicsRayTestResult.getHitNormalLocal();
				this.onGround = true;
			}
		}
	}
	if (closest * length > height + 0.4f) {
		this.onGround = false;
	}
}

}

[/java]

(The ground ray is longer to allow a more fine sliding speed)

1 Like

does BetterCharacterControl work on android, it doesn’t for me. for instance, the jbullet WALL sample works fine and I got the audio to work also. is there some special prep that must be done for android. anyone, thank you!

@gogita said: does BetterCharacterControl work on android, it doesn't for me. for instance, the jbullet WALL sample works fine and I got the audio to work also. is there some special prep that must be done for android. anyone, thank you!

Define “doesn’t work”.

does BetterCharacterControl work on android, running on droidx with 2.3.4 , compiled to 2.3.3 , runs for 1 second then throws itself off.
I tried to remove offensive parts of program. I recall seeing on a blog that there is an issue with the asset pack on android but wasn’t that just with the audio files? present version looks like this. works fine on desk top, thank you.


[java]
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.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
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.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 TestBetterCharacter extends SimpleApplication implements ActionListener {

    private BulletAppState bulletAppState;
    private BetterCharacterControl physicsCharacter;
    private Node characterNode;
    private CameraNode camNode;
    boolean rotate = false;
    private Vector3f walkDirection = new Vector3f(0, 0, 0);
    private Vector3f viewDirection = new Vector3f(0, 0, 1);
    boolean leftStrafe = false, rightStrafe = false, forward = false, backward = false,
    leftRotate = false, rightRotate = false;
    private Vector3f normalGravity = new Vector3f(0, -9.81f, 0);
    private Geometry planet;

    public static void main(String[] args) {
    TestBetterCharacter app = new TestBetterCharacter();
    // AppSettings settings = new AppSettings(true);
    // settings.setRenderer(AppSettings.LWJGL_OPENGL2);
    // settings.setAudioRenderer(AppSettings.LWJGL_OPENAL);
    // settings.setRenderer(AppSettings.LWJGL_OPENGL_ANY);
    // settings.setAudioRenderer(AppSettings.ANDROID_OPENAL_SOFT);
    // app.setSettings(settings);
    app.start();
    }

    @Override
    public void simpleInitApp() {
    //setup keyboard mapping
    setupKeys();

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

//welt 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);
    
    physicsCharacter.setJumpForce(physicsCharacter.getJumpForce().mult(2));//welt!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    // Load model, attach to character node
    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);

    // Set forward camera node that follows the character, only used when
    // view is "locked"
    camNode = new CameraNode("CamNode", cam);
    camNode.setControlDir(ControlDirection.SpatialToCamera);
    camNode.setLocalTranslation(new Vector3f(0, 2, -6));
    Quaternion quat = new Quaternion();
    // These coordinates are local, the camNode is attached to the character node!
    quat.lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
    camNode.setLocalRotation(quat);
    characterNode.attachChild(camNode);
    // Disable by default, can be enabled via keyboard shortcut
    camNode.setEnabled(false);
}

@Override
public void simpleUpdate(float tpf) {
    // Apply planet gravity to character if close enough (see below)
    checkPlanetGravity();

    // Get current forward and left vectors of model by using its rotation
    // to rotate the unit vectors
    Vector3f modelForwardDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_Z);
    Vector3f modelLeftDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_X);

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

    // 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);
    }
    physicsCharacter.setViewDirection(viewDirection);
    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();
}

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);
    }
}
private boolean lockView = false;

private void setupKeys() {
    inputManager.addMapping("Strafe Left",
            new KeyTrigger(KeyInput.KEY_U),
            new KeyTrigger(KeyInput.KEY_Z));
    inputManager.addMapping("Strafe Right",
            new KeyTrigger(KeyInput.KEY_O),
            new KeyTrigger(KeyInput.KEY_X));
    inputManager.addMapping("Rotate Left",
            new KeyTrigger(KeyInput.KEY_J),
            new KeyTrigger(KeyInput.KEY_LEFT));
    inputManager.addMapping("Rotate Right",
            new KeyTrigger(KeyInput.KEY_L),
            new KeyTrigger(KeyInput.KEY_RIGHT));
    inputManager.addMapping("Walk Forward",
            new KeyTrigger(KeyInput.KEY_I),
            new KeyTrigger(KeyInput.KEY_UP));
    inputManager.addMapping("Walk Backward",
            new KeyTrigger(KeyInput.KEY_K),
            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.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");
}[/java]

For next time:

it is normal that the BetterCharacterControl is stuck in mid-air in the wall when i perform a jump and then move forward in the wall, if i dont release the key i can stay there for ever like spider man, it doesnt happen with ControlCharacter.

Well yeah kinda,
I use a modified version, where the friction is set to 0 while no ground contact is being made, that fixes it. (But might create other problems, so this is no general solution for all kinds of games)

2 Likes

I use a dual ray check to see if there’s something in front of it; the method is similar to checkOnGround(); but it’s… modified. Probably not the best solution, but it works…

Inside the prePhysicsTick() function when it applies the velocity, I have this:
[java]checkSomethingInFront();
if(!somethingInFront){
rigidBody.setLinearVelocity(velocity);
}[/java]

and then checkSomethingInFront():
[java]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;
}
}

    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);
    for (PhysicsRayTestResult physicsRayTestResult : results) {
        if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)&amp;&amp;physicsRayTestResult.getCollisionObject()instanceof RigidBodyControl) {
           somethingInFront = true;
            return;
        }
    }
    somethingInFront = false;
}[/java] 

I also replaced the ray check for checkOnGround() with a sweep test, which is of course far more accurate but a little more expensive; it works pretty well. But anyways, hopefully that helps…

EDIT:
stepHeight is kinda not well named… It’s basically how high off the base of the character that the rays end so that you can still go up stairs and such and it won’t return that there’s something in front of the character; otherwise you wouldn’t be able to go up them without jumping!

1 Like

I can see everybody posting here their improvements to the BetterCharacterControl so I would like to share my tweaks too. I solved (at least is working for me :stuck_out_tongue:) two issues with this control:

  1. Stucking over stairs and any kind of slopes.
  2. Stucking when it goes over a wall (just changing the character friction when isn’t in ground).

Walking over stairs:

protected void checkSlope() {
    TempVars vars = TempVars.get();
    Vector3f rayFrom = vars.vect1;
    Vector3f rayTo = vars.vect2;
    Vector3f feetLoc = vars.vect3;
    Vector3f aux = vars.vect4;

    feetLoc.set(location);
    rayFrom.set(feetLoc).addLocal(aux.set(localUp).multLocal(slopeDepth));//.addLocal(localUp.mult(0.05f));
    rayTo.set(rayFrom).addLocal(aux.set(getWalkDirection()).normalizeLocal().multLocal(getFinalRadius())).addLocal(aux.set(localUp).mult(maxAcclivity));


    List<PhysicsRayTestResult> results = space.rayTest(rayFrom, rayTo);

    for (PhysicsRayTestResult physicsRayTestResult : results) {
        if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)) {
            rayTo.set(rayFrom.addLocal(rayTo.subtractLocal(rayFrom).multLocal(physicsRayTestResult.getHitFraction())));
            rayTo.addLocal(aux.set(getWalkDirection()).normalizeLocal().multLocal(slopeDepth));

            results = space.rayTest(rayFrom.set(rayTo).addLocal(aux.set(localUp).multLocal(maxStepHeight)), rayTo);

            if(!results.isEmpty()) {
                rayFrom.addLocal(rayTo.subtractLocal(rayFrom).multLocal(results.get(0).getHitFraction()));
                warp(location.setY(rayFrom.getY()));
            }

            vars.release();
            return;
        }
    }
    vars.release();
}

The character can only go over a slope if there is no obstacle just over that slope so a “checkSomethingInFront” must be done:

protected void checkSomethingInFront() {
    TempVars vars = TempVars.get();
    Vector3f rayFrom = vars.vect1;
    Vector3f rayTo = vars.vect2;
    Vector3f aux = vars.vect3;
    Vector3f aux2 = vars.vect4;

    aux.set(location).addLocal(aux2.set(localUp).multLocal(getFinalHeight()));
    rayFrom.set(aux);

    rayTo.set(aux.addLocal(rayTo.set(walkDirection).normalizeLocal().multLocal(getFinalRadius() + 0.05f)));

    List<PhysicsRayTestResult> results = space.rayTest(rayFrom, rayTo);

    for (PhysicsRayTestResult physicsRayTestResult : results) {
        if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)) {
            somethingInFront = true;
            vars.release();
            return;
        }
    }

    rayFrom.set(aux);
    rayTo.set(rayFrom).subtractLocal(aux.set(localUp).multLocal(getFinalHeight() - maxStepHeight));

    results = space.rayTest(rayFrom, rayTo);

    vars.release();

    for (PhysicsRayTestResult physicsRayTestResult : results) {
        if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)) {
            somethingInFront = true;
            return;
        }
    }

    somethingInFront = false;
}

To use it just:

@Override
public void prePhysicsTick(PhysicsSpace space, float tpf) {
    super.prePhysicsTick(space, tpf);

    checkSomethingInFront(); // Checks if something is in front of the character
    if(!somethingInFront) checkSlope(); // If not, checks if there is a slope and goes up it

    // This is a "botched fast fix" for the character getting stuck on a wall
    if(onGround) { if(rigidBody.getFriction() != friction) rigidBody.setFriction(friction); }
    else if(rigidBody.getFriction() != 0) rigidBody.setFriction(0f);
}

The initial values of the variables used on the checks are:

float maxStepHeight = 0.5f;
float maxAcclivity = 0.13f;
float slopeDepth = 0.05f;

The acclivity is used to avoid unnecessary relocations on ramps (kind of false slope positives).

3 Likes

Is BetterCharacterControl the best to use for simulation of a racing car, if not, what are the alternatives ?

The physicsvehicle?

I’m working on an improved version of the BetterCharacterControl, intending to fix common issues and incorporate some of the ideas that were mentioned here. My personal goals are:

  • no more bouncing up in the air when running over rubble (assuming a walker profile, see below)
  • no staying stuck in a wall when jumping against it (assuming a walker profile, see below)
  • support for movement types, like walker, hover, etc. You can specify a movement type and the control will do corresponding adjustments. Focusing on implementing the walker profile for now.
  • rudimentary support for other CollisionShapes
  • no sliding down ramps, angle configurable (assuming walker profile)
  • no walking up ramps of up to 90°, angle configurable
  • allowing an optional setViewDirection that is decoupled from the frame rate, so that setWalkDirection and setViewDirection are consistent for the user
  • supporting both relative and absolute vectors for both rotation and movement, getters as well as setters
  • supporting physical sleeping (and providing a wake up method)
  • real acceleration

Is there anything I need to know? Like monkeys already doing these things while I’m talking, so all my work would be obsolete in two weeks?

Also, I take wish lists. If you know a common problem with the current implementation, let me know and I’ll take a look into it (I’m new, so I don’t know what is a common problem and what is not).
As mentioned, I will take into account the things that have been mentioned here.

You may have this backwards or I’m reading it wrong? setViewDirection() should be a unit vector… it’s not frame dependent at all. setWalkDirection() as I understand it is a misnamed setWalkVelocity() method… ie: magnitude controls speed.

As far as I know nobody on the core is working on it.
Sounds cool so far! I actually encountered some of the issues you mentioned, so improvements would definitely help.

WalkDirection doesn’t need to be framerate decoupled, its a velocity vector and bullet itself is already framerate decoupled. Looks like you do something else wrong or get framerates below 10fps, see setMaxSubSteps in PhysicsSpace for that.

I apologize for being unclear about the framerate decoupling. Everything is fine with setWalkDirection. I just want the setViewDirection to be internally decoupled too, because it’s more consistent for the user of the class if both movement and rotation behave the same. It caused annoyance for me (because I didn’t notice for quite some time), and I’m sure I’m not the only one.

current simpleUpdate:

  1. calculate vectors based on input
  2. setWalkdirection (walkDir.mult(WALK_SPEED));
  3. setViewDirection (viewDir.mult(TURN_SPEED * tpf));

soon:

  1. calculate vectors based on input)
  2. setWalkdirection (walkDir.mult(WALK_SPEED));
  3. setViewDirection (viewDir.mult(TURN_SPEED));

Thats not a good idea. setViewDirection is instant so theres no need to decouple it. If the user does something that is instant its obvious that he has to decouple it. Just like lookAt, setLocalTranslation etc. are all not framerate decoupled either.

To further explain, if the user sets the viewDirection based on e.g. another spatial that the character should look at each frame decoupling internally would lead to unexpected results.

Good point. Although I still think that “it’s obvious” is a bold claim, because it wasn’t to me. I thought “oh great, this control already handles the frame rate thing, so I can just tell it where to look and where to go and everything will be fine”.

You are right, the instant change must stay. I’ll put more detailed thought in it when I actually write the lines. Shouldn’t be too hard to come up with a solution that allows both while still reducing the risk of mistakes (by demanding less knowledge about the internals).

I don’t see how you would do it anyway. In your example you already decouple a user variable (namely the viewDirection he created before or got from the character before). If at all you could add a changeViewDirection(Quaternion changeAmount,bool decoupled).