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.