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 sgold@sonic.net
*/
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, ≥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 → pressed, false → released
* @param tpf the time per frame (in seconds, ≥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 (≥0, ≤1)
* @param green the desired reflectivity for green light (≥0, ≤1)
* @param blue the desired reflectivity for blue light (≥0, ≤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;
}
}