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.
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.
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.
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
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):
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();
}
}
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);
}
}