BetterCharacterController bouncing off of the ground while moving

Hello folks,
I’m a JME newbie, and I’m having some trouble with my character controls. I’m using BetterCharacterController to control a 3rd person character. I calculate the walk direction, explicitly setting the Y portion of the vector to 0, normalize, multiply by my movement speed factor, and call setWalkDirection() to apply it. Despite the Y portion of my movement direction vector being 0, my character is bouncing off of the ground while moving around a flat Terrain plane. This is a problem, as I’m designing a platforming game and the player needs to be able to jump while moving, but if the player is bouncing off of the ground, then jumping becomes unreliable.

Here’s the full update() method:

    public void update(float tpf){
        super.update(tpf);
        //first, calculate the walking movement based on input
        walkDirection.set(0,0,0);
        
        Vector3f camAdjust_For = Vector3f.UNIT_Z;
        Vector3f camAdjust_Left = Vector3f.UNIT_X;
        
        if (cam != null){
            Quaternion cameraRot = cam.getRotation();
            camAdjust_For = cameraRot.mult(Vector3f.UNIT_Z).multLocal(1,0,1);
            camAdjust_Left = cameraRot.mult(Vector3f.UNIT_X);
        }
        
        if(controls.get(Controls.FORWARD))
            walkDirection.addLocal(camAdjust_For);
        if(controls.get(Controls.BACKWARD))
            walkDirection.addLocal(camAdjust_For.negateLocal());
        if(controls.get(Controls.STRAFE_L))
            walkDirection.addLocal(camAdjust_Left);
        if(controls.get(Controls.STRAFE_R))
            walkDirection.addLocal(camAdjust_Left.negate());
        
        walkDirection.setY(0.0f);
        // we now have our lateral movement fully calculated in walkDirection 
        
        
        walkDirection.normalizeLocal(); // to prevent the "fast diagonal" problem
        if(! walkDirection.equals(Vector3f.ZERO)){
            turnToward(walkDirection);
        }
        
        walkDirection.multLocal(WALK_SPEED);
        // if the player is holding run, multiply the speed by the run factor and message the animation controller (tbd)
        if(controls.get(Controls.RUN)){
            walkDirection.multLocal(RUN_FACTOR); // FIXME also implement run animation here
        }
        else{
            int NO_OP; // FIXME implement walk animation here
        }
        
        if( controls.get(Controls.JUMP) && onGround ){
            this.jump(); //FIXME may need to go before walk/run for animation reasons
        }
        
        Vector3f lastWalkVector = this.getWalkDirection();
        Vector3f newWalkVector = FastMath.interpolateLinear(ACCEL, lastWalkVector, walkDirection); //smoothen acceleration
        
        
        this.setWalkDirection(newWalkVector);
        System.out.println("Walk Direction: " + walkDirection); //FIXME for debug purposes
        System.out.println("Location: " + this.location);
        System.out.println("On Ground: " + onGround);

Here’s what a readout of those last few print statements looks like when the character bounces:

Walk Direction: (49.998833, 0.0, 0.34179902)
Location: (13.680224, -9.982346, 0.111574106)
On Ground: true
Walk Direction: (49.998833, 0.0, 0.34179902)
Location: (14.4202585, -9.84307, 0.12923567)
On Ground: true
Walk Direction: (49.998833, 0.0, 0.34179902)
Location: (15.252476, -9.70935, 0.13618508)
On Ground: false
Walk Direction: (49.998833, 0.0, 0.34179902)
Location: (16.085136, -9.581185, 0.14200328)
On Ground: false

Am I doing something wrong with my movement code? Are my physics values (gravity, mass, etc.) just poorly set?

Is it one giant quad?

Accuracy over large surfaces suffers and it’s sometimes better to break it up.

Hard to say since we can’t see them.

1 Like

Probably an issue, as @pspeed mentioned, with the quad of the terrain being larger than the collision shape. See this really old topic of mine: BetterCharacterControl acts like trampoline? - Troubleshooting / physics - jMonkeyEngine Hub

I used a Terrain object with size 256, so it’s not one large quad. In the debug view I can see a lot of smaller triangles.

I was able to improve things just by increasing the gravity and jumping force, but it’s still not very consistent. The problem also only shows up when the character is moving at fairly high speeds (movement vector of roughly ~50 for a 2 by 5 by 2 spatial).

okay, so I actually tested this WITH a large flat plane and what do you know, it worked! Now my running theory is that my character was somehow colliding with the edges of the terrain’s tris, causing them to bounce into the air. I’m on a time crunch, so I’ll just work without terrain for this project.

1 Like

If you’re using jme3-jbullet or jme3-bullet for physics, you should probably switch to Minie:

I can’t say whether switching would’ve solved the issue you were seeing, though.

I’ve seen this question asked many times and I had the same issue myself, but I could never find a working solution using a capsule as the collision shape (like the BCC does). Always end up with jumpy movement.

My best solution so far is to drop the capsule/BCC altogether and do like this:

  • use a cylinder as rigid body shape
  • disable gravity for this rigid body
  • each tick test a ray downwards and measure the distance to the ground
  • if distance is less that a defined threshold move the cylinder higher
  • if distance is greater than the threshold calculate downward velocity manually

There can’t be jumps over invisible lines this way because there is no friction with the ground, also moving over steps is smooth and there no more weird hops.
For me this feels way better even if its not the most accurate simulation and gives a much tighter control. Between accurate simulation and tight controls I lean to the latter.

I’m sure many games must do something like this but I don’t know if this has a name.

2 Likes

How do you handle slopes and stairs?

(I mean with your system specifically, not overall.)

1 Like

There are two basic approaches:

  1. I don’t want real physics, just move where I point and stay on the ground: that’s the regular character control.
  2. I want to be knocked around by things that bump into me and if the thing I’m standing on is moving, I should move, too. That’s better character control.

…as soon as you start disabling the physics of better character control then I’m not sure how it’s “better” than just using the regular character control.

That BCC is sensitive to bumps on the ground has a lot to do with physics accuracy, expectations, etc… (It’s also got many of its own problems besides that.) But if you want to get knocked around by other objects in a semi-realistic way then dealing with “tiny bumps” on the ground is going to be something to work around.

the gap between the floor and the cylinder is the maximum height of the staircase steps, e.g. if you decide steps under 0.5 units can be walked over without jumping that’s the distance you keep the cylinder floating above ground.
And the max slope is the hypotenuse between the gap and the radius of the cylinder.

1 Like

I don’t want to tell y’all how to do your jobs (and I don’t want to break backwards compatibility or anything) but I feel like it’s hard for newcomers to set expectations accordingly if one is called ‘thing’ and the other is called ‘the better version of thing’ when it’s only better in certain circumstances and the differences aren’t well described by the primary onboarding tutorials (at least in my opinion).
< /pedantic>
Still, the fact that the physics object is catching on a 180 degree angle seems like a bit of an issue if physics accuracy is the goal here. Are there settings internal to the Bullet state that I can mess with? Changing to a supplementary library like Minie might be useful when I get my feet under me, but right now I’m just getting a demo project together and I’d rather stick to what the engine has built in.

1 Like

I agree. I didn’t write it. Don’t use it. Look forward to patches.

1 Like

As you surmised, BetterCharacterControl is retained mainly for compatibility reasons.

I view BCC as a user-contributed example of how to create a custom physics control. Once you understand how it works, it should inspire you to mess with it and/or create your own character control. (That’s how I presented it in the Minie documentation.)

I regret the state of the JME wiki. Much of its content is outdated, or has been edited by well-intentioned people lacking in expertise. The good news is that the wiki is easy to edit.

2 Likes

I agree, I went through the same process with BCC and ended up making my own non-physicsState based character control after trying to fix BCC with no luck. I recall messing with friction and other physics settings to make the character less bouncy, but that made the character stick to walls and do other weird things. It felt like one of those situations where every time I fixed one issue, it caused another new issue.

So I also agree that it would be beneficial to mention in the wiki that BCC currently isn’t a clean physics control for use in real projects.

The optimal solution would be for someone to make a working character control so we can replace BCC and CharacterCotnrol, since both have their issues that prevent them from being viable in anything past a test case. I feel as though asking new users to make their own basic physics control could cause them to choose a different engine that doesn’t require learning and editing the physics system for a basic character control.

Obviously this is easier said than done, but a good starting point would at least be to open up an issue / feature request on Github for a basic humanoid character control

And from a “designing the new thing” perspective, once you start to ask the question “Why is this even a control?” then things start to unravel a bit.

1 Like

You can do that. The only physics engine built into JMonkeyEngine v3.4 is jme3-jbullet. The people who developed jme3-jbullet are no longer involved in JME. These days, I’d be surprised if anyone volunteered to troubleshoot an issue with jme3-jbullet. (It’s possible the issue you’re seeing isn’t specific to jme3-jbullet, but that’s unclear from the discussion so far.)

Minie does almost everything jme3-jbullet does. In particular, it includes BetterCharacterController. I’m actively maintaining Minie and interested in troubleshooting issues with it. So perhaps Minie would be a better starting point for your demo than jme3-jbullet.

1 Like

Which version of JME are you using and what are your bullet-related dependencies?

I haven’t found any documentation of the underlying issue at GitHub. That’s surprising for a bug in an essential feature like physics characters, particularly one that many people have encountered during the past 8 years.

I’d like to open an issue, but first I want to reproduce it. I’ve tried, but so far haven’t had any success.

Would someone please provide a simple self-contained example that demonstrates the bouncing issue—not a video or a code fragment, but something I can run?

EDIT—Here’s a starting point you can modify:

package jme3test.bullet;

import com.jme3.anim.AnimComposer;
import com.jme3.anim.tween.action.Action;
import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.Materials;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.shadow.DirectionalLightShadowRenderer;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.system.AppSettings;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.heightmap.HeightMap;
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;

/**
 * An example of character physics using Oto and BetterCharacterControl.
 *
 * Press the U/H/J/K keys to walk.
 *
 * @author Stephen Gold [email protected]
 */
public class TestOtoBcc
        extends SimpleApplication
        implements ActionListener {
    // *************************************************************************
    // fields

    private Action standAction;
    private Action walkAction;
    private AnimComposer composer;
    private BetterCharacterControl character;
    /**
     * true when the U key is pressed, otherwise false
     */
    private volatile boolean walkAway;
    /**
     * true when the H key is pressed, otherwise false
     */
    private volatile boolean walkLeft;
    /**
     * true when the K key is pressed, otherwise false
     */
    private volatile boolean walkRight;
    /**
     * true when the J key is pressed, otherwise false
     */
    private volatile boolean walkToward;

    final private Node translationNode = new Node("translation node");
    // *************************************************************************
    // new methods exposed

    /**
     * Main entry point for the application.
     *
     * @param ignored array of command-line arguments (not null)
     */
    public static void main(String[] ignored) {
        TestOtoBcc application = new TestOtoBcc();

        // Enable gamma correction for accurate lighting.
        boolean loadDefaults = true;
        AppSettings settings = new AppSettings(loadDefaults);
        settings.setGammaCorrection(true);
        application.setSettings(settings);

        application.start();
    }
    // *************************************************************************
    // SimpleApplication methods

    /**
     * Initialize this application.
     */
    @Override
    public void simpleInitApp() {
        addLighting(rootNode);
        configureCamera();
        configureInput();
        PhysicsSpace physicsSpace = configurePhysics();

        // Load the Oto model and find its animation actions.
        Spatial oto = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
        composer = oto.getControl(AnimComposer.class);
        standAction = composer.action("stand");
        walkAction = composer.action("Walk");

        // Attach the model model to a translation node.
        rootNode.attachChild(translationNode);
        translationNode.attachChild(oto);
        oto.move(0f, 5f, 0f);

        // Create the PhysicsControl and add it to the scene and space.
        float characterRadius = 3f;
        float characterHeight = 10f;
        float characterMass = 70f;
        character = new BetterCharacterControl(characterRadius, characterHeight,
                characterMass);
        translationNode.addControl(character);
        physicsSpace.add(character);

        character.warp(new Vector3f(-73.6f, 14.09f, -45.58f));
        addGround(physicsSpace, "terrain");
    }

    /**
     * Callback invoked once per frame.
     *
     * @param tpf the time interval between frames (in seconds, &ge;0)
     */
    @Override
    public void simpleUpdate(float tpf) {
        // Determine horizontal directions relative to the camera orientation.
        Vector3f away = cam.getDirection();
        away.y = 0;
        away.normalizeLocal();

        Vector3f left = cam.getLeft();
        left.y = 0;
        left.normalizeLocal();

        // Determine the walk velocity from keyboard inputs.
        Vector3f direction = new Vector3f();
        if (walkAway) {
            direction.addLocal(away);
        }
        if (walkLeft) {
            direction.addLocal(left);
        }
        if (walkRight) {
            direction.subtractLocal(left);
        }
        if (walkToward) {
            direction.subtractLocal(away);
        }
        direction.normalizeLocal();
        float walkSpeed = 7f;
        Vector3f walkVelocity = direction.mult(walkSpeed);
        character.setWalkDirection(walkVelocity);

        // Update the animation action.
        Action action = composer.getCurrentAction();
        if (walkVelocity.length() < 0.001f) {
            if (action != standAction) {
                composer.setCurrentAction("stand");
            }
        } else {
            character.setViewDirection(direction);
            if (action != walkAction) {
                composer.setCurrentAction("Walk");
            }
        }
    }
    // *************************************************************************
    // ActionListener methods

    /**
     * Callback to handle keyboard input events.
     *
     * @param action the name of the input event
     * @param ongoing true &rarr; pressed, false &rarr; released
     * @param tpf the time per frame (in seconds, &ge;0)
     */
    @Override
    public void onAction(String action, boolean ongoing, float tpf) {
        switch (action) {
            case "walk away":
                walkAway = ongoing;
                return;

            case "walk left":
                walkLeft = ongoing;
                return;

            case "walk right":
                walkRight = ongoing;
                return;

            case "walk toward":
                walkToward = ongoing;
                return;

            default:
                System.out.println("Unknown action: " + action);
        }
    }
    // *************************************************************************
    // private methods

    /**
     * Add lighting and shadows to the specified scene.
     */
    private void addLighting(Spatial scene) {
        scene.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);

        ColorRGBA ambientColor = new ColorRGBA(0.03f, 0.03f, 0.03f, 1f);
        AmbientLight ambient = new AmbientLight(ambientColor);
        scene.addLight(ambient);
        ambient.setName("ambient");

        ColorRGBA directColor = new ColorRGBA(0.3f, 0.3f, 0.3f, 1f);
        Vector3f direction = new Vector3f(-7f, -3f, -5f).normalizeLocal();
        DirectionalLight sun = new DirectionalLight(direction, directColor);
        scene.addLight(sun);
        sun.setName("sun");

        // Render shadows based on the directional light.
        viewPort.clearProcessors();
        int shadowMapSize = 2_048; // in pixels
        int numSplits = 3;
        DirectionalLightShadowRenderer dlsr
                = new DirectionalLightShadowRenderer(assetManager,
                        shadowMapSize, numSplits);
        dlsr.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
        dlsr.setEdgesThickness(5);
        dlsr.setLight(sun);
        dlsr.setShadowIntensity(0.4f);
        viewPort.addProcessor(dlsr);

        // Set the viewport's background color to light blue.
        ColorRGBA skyColor = new ColorRGBA(0.1f, 0.2f, 0.4f, 1f);
        viewPort.setBackgroundColor(skyColor);
    }

    /**
     * Add a heightfield body to the specified PhysicsSpace.
     *
     * @param physicsSpace (not null)
     */
    private void addGround(PhysicsSpace physicsSpace, String groundType) {
        Spatial ground;
        if (groundType.equalsIgnoreCase("terrain")) {
            String assetPath = "Textures/Terrain/splat/mountains512.png";
            Texture texture = assetManager.loadTexture(assetPath);
            Image image = texture.getImage();
            HeightMap heightMap = new ImageBasedHeightMap(image);
            heightMap.setHeightScale(0.2f);
            heightMap.load();
            ground = new TerrainQuad("terrain", 65, 513, heightMap.getHeightMap());
        } else {
            Mesh quad = new Quad(999f, 999f);
            ground = new Geometry("terrain", quad);
            ground.move(-500f, 14f, 500f);
            ground.rotate(-FastMath.HALF_PI, 0f, 0f);
        }
        rootNode.attachChild(ground);
        Material greenMaterial = createLitMaterial(0f, 0.5f, 0f);
        ground.setMaterial(greenMaterial);

        // Construct a static RigidBodyControl based on the ground spatial.
        CollisionShape shape = CollisionShapeFactory.createMeshShape(ground);
        RigidBodyControl rbc = new RigidBodyControl(shape, 0f);
        rbc.setPhysicsSpace(physicsSpace);
        ground.addControl(rbc);
    }

    /**
     * Configure the Camera during startup.
     */
    private void configureCamera() {
        flyCam.setMoveSpeed(10f);

        cam.setLocation(new Vector3f(-39f, 34f, -47f));
        cam.setRotation(new Quaternion(0.183f, -0.68302f, 0.183f, 0.68302f));
    }

    /**
     * Configure keyboard input during startup.
     */
    private void configureInput() {
        inputManager.addMapping("walk away", new KeyTrigger(KeyInput.KEY_U));
        inputManager.addMapping("walk left", new KeyTrigger(KeyInput.KEY_H));
        inputManager.addMapping("walk right", new KeyTrigger(KeyInput.KEY_K));
        inputManager.addMapping("walk toward", new KeyTrigger(KeyInput.KEY_J));
        inputManager.addListener(this,
                "walk away", "walk left", "walk right", "walk toward");
    }

    /**
     * Configure physics during startup.
     */
    private PhysicsSpace configurePhysics() {
        BulletAppState bulletAppState = new BulletAppState();
        bulletAppState.setDebugEnabled(true);
        stateManager.attach(bulletAppState);
        PhysicsSpace result = bulletAppState.getPhysicsSpace();
        result.setGravity(new Vector3f(0f, -60f, 0f));

        return result;
    }

    /**
     * Create a single-sided lit material with the specified reflectivities.
     *
     * @param red the desired reflectivity for red light (&ge;0, &le;1)
     * @param green the desired reflectivity for green light (&ge;0, &le;1)
     * @param blue the desired reflectivity for blue light (&ge;0, &le;1)
     * @return a new instance (not null)
     */
    private Material createLitMaterial(float red, float green, float blue) {
        Material result = new Material(assetManager, Materials.LIGHTING);
        result.setBoolean("UseMaterialColors", true);

        float opacity = 1f;
        result.setColor("Ambient", new ColorRGBA(red, green, blue, opacity));
        result.setColor("Diffuse", new ColorRGBA(red, green, blue, opacity));

        return result;
    }
}
1 Like