Extends CharacterControl with good ground detection

Hello.

I’m new to JME and try to extends the CharacterControl class by a ground detection. I think the Ray is a worse choice, since it is just a line, while the CapsuleCollisionShape has a volume and the ground check x/z dimension must be >= the Character Shape x/z Dimension. An other way could be to use more, say 8 Rays (respectively one Ray 8 times) around the Capsule hull, but this sounds like very much work for the CPU.

Since most of the Methods of important Classes of Bullet/Minie like CharacterControl, BetterCharacterControl and RigidBodyControl ends with native Calls, I cannot look into how this works in general.

Another Idea was to use BoundingBox below the Capsule to test the ground collisions, but it seems it doesn’t collide with TerrainQuad and I found no useful informations about the Bounding classes.

So has somebody a general hint/idea how to realize that?

Edit: The reason why I want to do is, CharacterControl#onGround() returns only false, while the Character jumps. When the Character runs over a edge and falls down, I have no glue how to switch to a “fall” state to block walking and jumping while in the air.

Thank you, kind regards

1 Like

Welcome to the JMonkeyEngine community, Nakano.

I’m sure that CharacterControl can be improved upon. Even though our examples use it on uneven terrain, I suspect it was originally intended for use on flat surfaces.

Since most of the Methods of important Classes of Bullet/Minie like CharacterControl, BetterCharacterControl and RigidBodyControl ends with native Calls, I cannot look into how this works in general.

All the native code is published here: https://github.com/stephengold/Libbulletjme

The native class most relevant to CharacterControl is btKinematicCharacterController

There’s a high-level description of how btKinematicCharacterController works on page 26 of the Bullet User Manual.

So has somebody a general hint/idea how to realize that?

The best starting point for improvements would be BetterCharacterControl.

To avoid the need for multiple raycasts, I suggest using a sweep test or contact test with an auxiliary collision shape. For explanations of sweep tests and contact tests, see the Minie documentation regarding intersection tests.

CharacterControl#onGround() returns only false, while the Character jumps.

Which version of the physics library are you using? I ask because there was a bug in onGround() that was fixed in Minie v5.0.0, and I wonder if the behavior you’re seeing might be related to that issue or its fix.

1 Like

If you want a very “homemade” solution (I don’t know if it works, I thought it fast) you can start an arrow -y like raycasting and go down a given length of your choice, from there start 2,3 ,4 etc… arrows and check if they collide, I don’t know if it’s possible and how much it impacts on the cpu

1 Like

Thank you very much

I tried BetterCharacterControl first, but CharacterControl runned smoother for me. For example I found not good settings. If I used low Gravity, the Spatial flied over upward Terrains like a really fast car, if I increased the Gravity, the spatial falled througt the Terrain. And it miss also the stepHeight.

I’m using Minie 6.0.0.

I may found the reason, why the collision check between BoundingBox and Terrain didn’t worked. It seems to be a Bug with scaled Terrain along x and z axis. (Okay, it is more likely I do something wrong than finding as newbit and not really good coder a bug ^^)

The Terrain I use is scalled by 10 * 1 * 10 and has a Patchsize of 33.

In TerrainQuad#collideWith(Collidable, CollisionResults), it calls the same method for all childs.
In TerrainPatch#collideWith(Collidable, CollisionResults), it calls TerrainPatch#collideWithBoundingVolume(BoundingVolume, CollisionResults) which calls TerrainPatch#collideWithBoundingBox(BoundingBox, CollisionResults).

collideWithBoundingBox calls 4 times TerrainPatch#worldCoordinateToLocal(Vector3f):

Vector3f topLeft = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x-bbox.getXExtent(), 0, bbox.getCenter().z-bbox.getZExtent()));
Vector3f topRight = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x+bbox.getXExtent(), 0, bbox.getCenter().z-bbox.getZExtent()));
Vector3f bottomLeft = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x-bbox.getXExtent(), 0, bbox.getCenter().z+bbox.getZExtent()));
Vector3f bottomRight = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x+bbox.getXExtent(), 0, bbox.getCenter().z+bbox.getZExtent()));

I think, this should calculate the Patch index (0 to 32 in this case) to get the Triangles at this location.

If I’m right up to here, then the formula is may wrong. The localTransform of this Patch is

translate: -32, 0, -32
scale: 1, 1, 1

The worldTransform is

translate: -320, 0, -320
scale: 10, 1, 10

I think the Formula should divide the worldTranslation by the worldScale, not the coordinates, should it? (The rotation is also not used)
The first call calculates for a “BoundingBox [Center: (-1.7272598, 0.0, -57.552147) xExtent: 0.2 yExtent: 0.1 zExtent: 0.2]” the Vector “(319.80728, 0.0, 330.2248)”, hence far bigger than the Patch.

After setting the Terrain scale to 1 * 1 * 1, the Box collide with the Terrain as expected.

I created a Test for the scaling problem. It creates a flat terrain with the AlphaMap/Textures from testdata to make it good visible, a BoundingBox with center -20, 0, -20 and extends 1, 10, 1, a Geometry with Box mesh and the location/size from the BoundingBox to show the BoundingBox location, a Text and a ChaseCam on the Geometry. The Terrain is initial scaled by factor 1, a press on the space key scale it to 10 * 1 * 10, a second press scale it back. An AppState check the collision and refresh the text.

While the Terrain is scaled by 1, it collide, while it is scaled 10 * 1 * 10, it doesn’t.

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.bounding.BoundingBox;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.font.LineWrapMode;
import com.jme3.input.ChaseCamera;
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.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;

public final class BoundingBoxTerrainCollideTest extends SimpleApplication {
	@Override
	public void simpleInitApp() {
		getRootNode().addLight(new AmbientLight(ColorRGBA.White));
		
		final TerrainQuad terrain = createTerrain();
		getRootNode().attachChild(terrain);
		
		final BoundingBox box = createBoundingBox();
		
		final BitmapText text = createText();
		text.setLocalTranslation(5, 200, 0);
		getGuiNode().attachChild(text);
		
		final Spatial visibleBox = createVisibleBox(box);
		getRootNode().attachChild(visibleBox);
		
		createCamera(visibleBox);
		
		getRootNode().updateGeometricState();
		
		getStateManager().attach(new AppState() {
			@Override
			public void update(final float tpf) {
				final CollisionResults results = new CollisionResults();
				terrain.collideWith(box, results);
				text.setText(results.size() > 0 ? "Collide" : "Don't collide");
			}
			
			@Override
			public void stateDetached(final AppStateManager stateManager) {
				
			}
			
			@Override
			public void stateAttached(final AppStateManager stateManager) {
				
			}
			
			@Override
			public void setEnabled(final boolean active) {
				
			}
			
			@Override
			public void render(final RenderManager rm) {
				
			}
			
			@Override
			public void postRender() {
				
			}
			
			@Override
			public boolean isInitialized() {
				return(true);
			}
			
			@Override
			public boolean isEnabled() {
				return(true);
			}
			
			@Override
			public void initialize(final AppStateManager stateManager, final Application app) {
				
			}
			
			@Override
			public String getId() {
				return("");
			}
			
			@Override
			public void cleanup() {
				
			}
		});
		
		getInputManager().addMapping("Scaling", new KeyTrigger(KeyInput.KEY_SPACE));
		getInputManager().addListener(new ActionListener() {
			@Override
			public void onAction(final String name, final boolean isPressed, final float tpf) {
				if(isPressed) {
					final boolean isBig = (terrain.getLocalScale().x == 10f);
					if(isBig) {
						terrain.setLocalScale(1f, 1f, 1f);
					} else {
						terrain.setLocalScale(10f, 10f, 10f);
					}
				}
			}
		}, "Scaling");
	}
	private BitmapText createText() {
		 final BitmapFont guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
		 final BitmapText text = new BitmapText(guiFont);
		 text.setLineWrapMode(LineWrapMode.Word);
		 return(text);

	}
	private BoundingBox createBoundingBox() {
		final BoundingBox box = new BoundingBox(new Vector3f(-20f, 0f, -20f), 1f, 10f, 1f);
		return(box);
	}
	private Spatial createVisibleBox(final BoundingBox bb) {
		final Box boxMesh = new Box(bb.getXExtent(), bb.getYExtent(), bb.getZExtent());
		
		final Material boxMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
		boxMat.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
		
		final Geometry box = new Geometry("Visible box", boxMesh);
		box.setLocalTranslation(bb.getCenter());
		box.setMaterial(boxMat);
		return(box);
	}
	private void createCamera(final Spatial s) {
		new ChaseCamera(getCamera(), s, getInputManager());
	}
	private TerrainQuad createTerrain() {
		final Material mat = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
		mat.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
		
		final Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
		grass.setWrap(WrapMode.Repeat);
		mat.setTexture("Tex1", grass);
		mat.setFloat("Tex1Scale", 64f);
		
		final Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
		dirt.setWrap(WrapMode.Repeat);
		mat.setTexture("Tex2", dirt);
		mat.setFloat("Tex2Scale", 32f);
		
		final Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
		rock.setWrap(WrapMode.Repeat);
		mat.setTexture("Tex3", rock);
		mat.setFloat("Tex3Scale", 128f);
		
		final TerrainQuad terrain = new TerrainQuad("Terrain", 33, 65, new float[65 * 65]);
		terrain.setMaterial(mat);
		
		return(terrain);
	}
	
	public static void main(final String[] args) {
		new BoundingBoxTerrainCollideTest().start();
	}
}
1 Like

I don’t know the cause of the issue you’re seeing, but I think you are on the wrong track.

Collidable.collideWith() is a scene-graph collision test, not physics test. Since your app includes physics, I recommend using physics tests…

  • For a ray test, use CollisionSpace.rayTestRaw()
  • For a sweep test, use CollisionSpace.sweepTest()
  • For a contact test, use CollisionSpace.contactTest()
2 Likes

Thank you very much. contactTest seems to works fine and very easy.
Happy new Year!

import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.shapes.ConvexShape;
import com.jme3.bullet.control.GhostControl;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;

/* TODO: More generic name, since it could be used for more than a Ground test */
public final class CharacterOnGroundContactTester implements /* Interface from own api */ {
	private boolean initalized;
	
	private GhostControl ghost;
	
	private final ConvexShape collisionShape;
	private final Spatial spatialToAttach;
	private final int staticCollidingObjectCount;
	private final BulletAppState bullet;
	
	public CharacterOnGroundContactTester(final ConvexShape collisionShape, final Spatial spatialToAttach,
			final int staticCollidingObjectCount, final BulletAppState bullet) {
		// null/less-than-zero-checks...
		this.collisionShape = collisionShape;
		this.spatialToAttach = spatialToAttach;
		this.staticCollidingObjectCount = staticCollidingObjectCount;
		this.bullet = bullet;
	}
	@Override
	public Boolean source() {
		if(!initalized) {
			doInit();
			initalized = true;
		}
		
		final int i = bullet.getPhysicsSpace().contactTest(ghost, null);
		return(i > staticCollidingObjectCount);
	}
	public PhysicsCollisionObject getCollisionObject() {
		return (ghost);
	}
	public void deinitialize() {
		if(initalized) {
			bullet.getPhysicsSpace().remove(ghost);
			spatialToAttach.removeControl(ghost);
			
			ghost = null;
			
			initalized = false;
		}
	}
	private void doInit() {
		ghost = new GhostControl(collisionShape);
		bullet.getPhysicsSpace().add(ghost);
		spatialToAttach.addControl(ghost);
	}
}
1 Like