Giving a BetterCharacterControl more realistic behaviour

Hi,

I’ve recently started a new project, and decided to use the new BetterCharacterControl instead of the old CharacterControl this time, but I’ve run into some problems which give the BetterCharacterControl a rather unrealistic behaviour:

  • when the player jumps against a wall and keeps walking towards it, the player ‘sticks’ to the wall, and stays there untill he stops walking towards the wall. It looks like the friction is way to high, but I couldn’t find a friction setting.

  • When the player walks over small bumps or steps on the ground, the player bounces over them, instead of stepping up them. This looks like the player is simply pushed into the steps and the capsule collision shape causes it to bounce over the steps. In the old CharacterControl, I could set a StepHeight variable to make the player step onto the steps instead, but the new one doesn’t seem to have such a feature.

To solve the 2nd problem, I tried making the stepheight feature myself by casting a ray straight down at playerNode.getWorldTranslation().add(walkDir).addLocal(0,2,0), calculating the height of a potential step with stepHeight = 2-result_of_raycasting.getClosestCollision().getDistance(), then warping the player to the location stepHeight higher than the current location. However, this caused the player to bounce even higher when walking over steps, and the player then sometimes started ‘vibrating’ when walking over completely flat areas.

For the 1st problem, I don’t know how to fix it.

Does anyone here know what I can do to improve the behaviour of this control?

Many thanks in advance.

As to the first problem, restating it in real-life terms: “When my feet leave the ground, the jet pack strapped to my back is still running and when pushing me into a wall at faster than human walk speed, I don’t slide down.”

Tip: turn off the jet pack when the feet leave the ground.

The problem with that is that it makes the player completely uncontrollable during jumps. So is there a way to check if the player is touching a wall in a similar way than player.isOnGround()?

I managed to solve the bouncing around problem, and almost managed to solve the sticking against walls problem. Does anyone want the code when I’m done?

Yes, please! I’d love the code! I don’t like the way my player sticks to walls. If you can find a fix for that I’d really appreciate it!

The final result is a bunch of messy code spread around a bunch of classes, so I’ll post the basic algorithm here. You should easily be able to code it yourself then:

To avoid the sticking to walls issue:
The main problem is, as previously described, that the player is pushed against the wall in mid-air as if he was wearing a jetpack.
The first step to reduce this issue was to give the player acceleration and decelleration, so that when the player walks against a wall, the game doesn’t react as if the player keeps puching agains it as if the player is running at full speed. For the acceleration/decelleration, I used this code (after where you set the walkDir vector):

//This part belongs in simpleUpdate
if (height <= 0.1f || stepping) {
    walkDir = walkDir.mult(FastMath.clamp(7 * tpf, 0, 1))
            .addLocal(walkDirPrev.mult(FastMath.clamp(1 - 7 * tpf, 0, 1)));
} else {
    walkDir = walkDir.mult(FastMath.clamp(tpf, 0, 1))
            .addLocal(walkDirPrev.mult(FastMath.clamp(1 - tpf, 0, 1)));
}
walkDirPrev = walkDir.clone();

The height in this code is the height of the player above the ground, which we use to detect if the player is standing on the ground (the GhostControl that I use for an other part of the player messes up onGround()).
The stepping boolean is used to detect if the player is walking up a step (which is a part of the code used to stop the bouncing of the player while walking over steps).

This should give the player a more natural behaviour when walking and jumping around, and it already partially fixes sticking to walls if the player first jumps, then walks towards a wall. Unfortunately, it can still cause the player to stick to walls in some cases, since it only bases the forces on the set velocities, not collisions or actual movements.
The solution to this is simple, right? Instead of using the walkDir vector from the previous frame, you simply use the distance the player actually moved between the previous frame and this one, and you automatically take the collision results from the physics engine into account with this.
Off course, there is a catch: Computers don’t work at an infinite accuracy, so extracting the theoretical walkDir vector based on the actual movement of the player between 2 frames and the time between them is rather inaccurate. I haven’t really found a way to do this with enough accuracy to make it completely relyable, but I have found a good workaround: use a GhostControl to check for collisions, and only extract the walkDir from the movement when a collision is detected. So this is the code I used to extract the walkDirection out of the player movement:

//This code belongs in simpleUpdate
if (collision) {
    walkDirPrev =
            playerNode.getWorldTranslation().subtract(playerPosPrev)
            .divideLocal(tpf * 8).multLocal(1, 0, 1);
}

playerPosPrev = playerNode.getWorldTranslation().clone();
collision = false;

The tpf*8 is because after getting the input, I normalize the walkDir and multiply by 8 to make the player walk at 8m/s (not normalizing the walkDir vector causes the player to go faster when you press both the forward and strafe key). Replace the 8 with the maximum velocity of the player in your own version. In this code, I always work with vectors with numbers in the [-1,1] range. You can easily work with [-maxSpeed,maxSpeed] as range in this code, just be consistent (in that case, just use tpf).

Off course, to make this work correctly, we need to know if the player is against a wall. I did this by creating the player like this:

//This code belongs were you create the player (simpleInit or an alternate loading method)
player = new BetterCharacterControl(0.4f, 1.8f, 60f);
playerNode = new Node("player");
playerNode.addControl(player);
CompoundCollisionShape ccs = new CompoundCollisionShape();
ccs.addChildShape(new CylinderCollisionShape(
        new Vector3f(0.5f, 0.6f, 0.5f), 1), Vector3f.UNIT_Y.mult(1f));
playerGhost = new GhostControl(ccs);
Node ghostNode = new Node("ghost_node");
ghostNode.addControl(playerGhost);
playerNode.attachChild(ghostNode);
bulletAppState.getPhysicsSpace().addCollisionListener(new PlayerCollision());

Now we have a GhostControl in the shape of a cylinder around the player. We can now detect the collisions in the collision listener like this:

//This code is a seperate class
public final class PlayerCollision implements PhysicsCollisionListener {
    @Override
    public final void collision(PhysicsCollisionEvent event) {
        if (event.getNodeA().getName().equals("ghost_node")) {
            if (!event.getNodeB().getName().equals("player")) {
                TLSApplication.app.collision = true;
            }
        } else if (event.getNodeB().getName().equals("ghost_node")) {
            if (!event.getNodeA().getName().equals("player")) {
                TLSApplication.app.collision = true;
            }
        }
    }
}

TLSApplication is the class in which I placed the code that handles the acceleration/deceleration stuff. ‘app’ is an instance of that class.

That’s the sticking to walls part. For it to fully work, you still need to get the player height by casing a ray straight down from the player, but it should be easy enough to figure out how to do that by yourself.

To avoid the ‘bouncing around’ issue:
In the previous part, I figured most things out with some math and logical thinking (though to be honest, a bit of trial and error was also involved). In this part, I also tried that, but failed. I mostly ended up tweaking formulas and variables untill I got something that worked for me, in my (test)level files. If it doesn’t work well for you, you might need to retry it a few times with slightly different numbers.

The problem is quite simple: while walking up stairs, the player basically walks against each step, and the collision engine makes it bounce back, taking the shape of the player into account. Especially at relatively high speeds (sprinting), it can give rather unrealistic results. To fix this problem, all we need to do is make the player move up a bit before walking against a step. We can do this by getting the height and distance to each potential step using ray casting, then feed that data to a formula that represents the path the player should follow when walking up the step, and adjust the player’s altitude a bit if the player is too low. If you want to modify this method a bit, then make sure the distance from which this code tries to help the player up a step is larger then the radius of the player’s collision shape.

Well, I told you this part was messy, but here’s my code:

if (walkDir.length() > 0.0f) {
    Ray r = new Ray();
    r.setDirection(Vector3f.UNIT_Y.negate());
    r.setOrigin(playerNode.getLocalTranslation()
            .add(walkDir.mult(0.5f / walkDir.length()))
            .addLocal(Vector3f.UNIT_Y));
    r.setLimit(1);
    CollisionResults result = new CollisionResults();
    WorldManager.world.collideWith(r, result);
    stepping = false;
    if (result.getClosestCollision() != null) {
        float step = 1 - result.getClosestCollision().getDistance();
        if (step > 0.08f && step < 0.45f) {
            //there is a step in front of the player, so try to find the distance to it
            try {
                if (step + height < 0.4) {
                    //only continue when the step is really small enough
                    step += height; //step is now the height of the full step, not the part
                                    //above the player
                    stepping = true;
                    Ray r2 = new Ray();
                    r2.setOrigin(playerNode.getWorldTranslation()
                            .add(0, step / 2, 0));
                    r2.setDirection(walkDir.normalize());
                    CollisionResults results2 = new CollisionResults();
                    rootNode.collideWith(r2, results2);
                    walkDir.addLocal(0, (step * FastMath.sqrt(1
                            - FastMath.sqr(results2.getClosestCollision()
                            .getDistance() / 0.5f)) - height) * 2, 0);
                    player.getVelocity().setY(0);
                    if (walkDir.y != walkDir.y) { //This apparently checks if walkDir.y is NaN...
                                                  //weird, right? But hey, it works!
                        walkDir.setY(0);
                    }
                }
            } catch (Exception ex) {
                Logger.getLogger("").warning("[stepping]Raycasting encountered an "
                        + "unexpected result!");
            }
        }
    }
}
if (walkDir.length() > 1) {
    walkDir.normalizeLocal();
}

This should fix the bouncing around problem. But it does only detect steps correctly if they have a modelled side.

If you have any problems with implementing this code or you don’t understand a part of it, feel free to ask.

BTW: sorry for the late reply. For some reason I didn’t recieve an email notification about your post so I didn’t know someone answered.