CharacterControl with BoxShape

Hi and good day,

If I use a CharacterControl and assign it a Box Collision Shape “walking” doesnt go fluently like with a capsuleshape on a heightmap terrain.

I think this is a bug?



EDIT: I also noticed CharacterControls can not collide with eachother? Then I might use a Ghost Control anyway, also since one can set the position of a Ghost Node.



Thanks for your time,

Kajos

Box shape doesnt make much sense since the character physics shape doesnt rotate.

Ah ok… But I gather a GhostControl does.

So I have the Ghost Control:

[java]

control.setPhysicsLocation(control.getPhysicsLocation().add(walkDirection));

if (control.getOverlappingCount() > 0) {

control.setPhysicsLocation(getPosition());

}



setPosition(control.getPhysicsLocation());

[/java]



However this makes the unit stutter and gets stuck in objects, Any help on how I can improve this?



Thanks,

Kajos

You don’t have to set the location of the control, just move the spatial.

I haven’t set addControl with the control, cause I wanted to detect “future” collisions, ie set the control to check collisions before I set the node so i can determine if the node is allowed to be there. I thought it was rather innovative, but somehow it doesnt work that well…

Oh, now I understand what your idea was… If something overlaps, just go back to the previous position, right? The problem is that the overlapping objects are only known after the next physics tick. To do this properly you will have to implement PhysicsTickListener.

Thank you very much, that works like a charm! Am I stupid for not finding that out by my self? Do you know that out of pure experience or am I overseeing some documentation source?

Well I wrote the bullet physics adaption :wink: But nobody told me that, I also had to check if the bullet method checks instantly or just returns a precompiled list from the last tick. Note however that the normal bullet Character does the same thing you do, what exactly is the problem apart from the box shape? Anyway, if you come to improve your character to a working state it would be cool if you share the code.

Yeah sure, anything for the JME community :wink:

Is there a way to change the position of a node in a physicstick?

You shouldn’t do that, no, because the physics update might run in parallel to rendering. Enqueue the call to the OpenGL thread.

Yeah figured that, forgot that it runs in thread XD

Okay, I really got the feeling I’m hitting a wall here. I have spent numerous hours in it now, and still can’t get whats wrong.

Let me explain: I made a class handling the collisions by implementing the ticklistener. At first it looks like it works, however in my case; I have two influences: gravity and the walkingdirection. It works with both at first untill the unit hits the ground and then only the walking direction works (fine btw).

[java] CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(0.1f, 1f, 1);

ArrayList<Vector3f> order = new ArrayList<Vector3f>();

order.add(new Vector3f(0,-0.3f)); // gravity

order.add(walkDirection);

Physics.bulletAppState.getPhysicsSpace().addTickListener(new CollisionDetection(capsuleShape, unitNode, null, order,main));[/java]



And the CollisionDetection class (should be called CollisionPrevention, but ok):

[java]/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.

    */



    package mygame.Physics;



    import com.jme3.bullet.PhysicsSpace;

    import com.jme3.bullet.PhysicsTickListener;

    import com.jme3.bullet.collision.PhysicsCollisionObject;

    import com.jme3.bullet.collision.shapes.CollisionShape;

    import com.jme3.bullet.control.GhostControl;

    import com.jme3.math.Vector3f;

    import com.jme3.scene.Node;

    import java.util.ArrayList;

    import java.util.concurrent.Callable;

    import mygame.Main;



    /**

    *
  • @author Kajos

    */

    public class CollisionDetection implements PhysicsTickListener {

    Node unit;

    private Main main;

    ArrayList<Vector3f> directions = new ArrayList<Vector3f>();

    public ArrayList<GhostControl> collisioncontrols = new ArrayList<GhostControl>();

    public ArrayList<PhysicsCollisionObject> skipObjects = new ArrayList<PhysicsCollisionObject>();



    public CollisionDetection(CollisionShape capsuleShape, Node unit, ArrayList<PhysicsCollisionObject> skipObjects, ArrayList<Vector3f> directions, Main main) {

    this.unit=unit;

    this.directions=directions;

    this.main=main;

    if (skipObjects != null) {

    this.skipObjects=skipObjects;

    }



    //control=new GhostControl(capsuleShape);

    //control.setPhysicsLocation(unit.getLocalTranslation());



    //this.skipObjects.add(control);

    //Physics.bulletAppState.getPhysicsSpace().add(control);



    for (int i = 0; i < directions.size(); i++) {

    GhostControl control = new GhostControl(capsuleShape);

    this.skipObjects.add((PhysicsCollisionObject)control);

    collisioncontrols.add(control);

    collisioncontrols.get(i).setPhysicsLocation(unit.getLocalTranslation());

    Physics.bulletAppState.getPhysicsSpace().add(control);

    }



    }



    public void prePhysicsTick(PhysicsSpace space, float f) {

    //control.setPhysicsLocation(unit.getLocalTranslation());

    Vector3f carryDirection = Vector3f.ZERO;

    for (int i = 0; i < collisioncontrols.size(); i++) {

    carryDirection = carryDirection.add(directions.get(i));//carryDirection.addLocal(directions.get(i));

    //collisioncontrols.get(i).setPhysicsLocation(control.getPhysicsLocation().add(directions.get(i))); // carryDirection

    collisioncontrols.get(i).setPhysicsLocation(unit.getLocalTranslation().add(directions.get(i))); // carryDirection

    }

    }

    Vector3f finalDirection;

    public void physicsTick(PhysicsSpace space, float f) {

    finalDirection = Vector3f.ZERO;



    for (int i = 0; i < collisioncontrols.size(); i++) {

    boolean nocollision = true;

    for (PhysicsCollisionObject object : collisioncontrols.get(i).getOverlappingObjects()) {

    if (!skipObjects.contains(object)) {

    nocollision = false;

    System.out.println("Collision for V3f: ");



    break;

    }

    }

    if (nocollision) finalDirection = finalDirection.add(directions.get(i));

    }

    //control.setPhysicsLocation(unit.getLocalTranslation());

    if (finalDirection != Vector3f.ZERO) {

    main.enqueue(new Callable() {

    public Object call() throws Exception {

    unit.setLocalTranslation(unit.getLocalTranslation().add(finalDirection));

    return true;

    }

    });

    }



    }

    }

    [/java]



    You ll see some comments where I desperately tried to change the code to find the error.



    Any help is, very, much appreciated.



    Kajos

Hm, I dont know what you try to achieve, but the gravity should stop being applied when it hits the ground, no? :slight_smile: Apart from that, I don’t see any checks for collision direction, maybe you want to check if the collision object is below the character? Maybe you can get some ideas from the original jbullet character class: http://code.google.com/p/jbullet-jme/source/browse/branches/jbullet/src/com/bulletphysics/dynamics/character/KinematicCharacterController.java

No, there’s something wrong with GhostControl → terrain collisions. Either that, or i’m implementing it wrong.



Here’s an example with TerrainCollisionTest, I just hacked it together, but you get the idea.



EDIT: in SimpleUpdate should be:

controlS.setPhysicsLocation(controlS.getPhysicsLocation().subtract(0,0.003f,0));



(but still that doesnt matter)



[java]/*

  • Copyright © 2009-2010 jMonkeyEngine
  • All rights reserved.

    *
  • Redistribution and use in source and binary forms, with or without
  • modification, are permitted provided that the following conditions are
  • met:

    *
    • Redistributions of source code must retain the above copyright
  • notice, this list of conditions and the following disclaimer.

    *
    • Redistributions in binary form must reproduce the above copyright
  • notice, this list of conditions and the following disclaimer in the
  • documentation and/or other materials provided with the distribution.

    *
    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors
  • may be used to endorse or promote products derived from this software
  • without specific prior written permission.

    *
  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  • "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    */

    package jme3test.terrain;



    import jme3tools.converters.ImageToAwt;

    import com.jme3.bullet.collision.shapes.SphereCollisionShape;

    import com.jme3.bounding.BoundingBox;

    import com.jme3.bullet.BulletAppState;

    import com.jme3.app.SimpleApplication;

    import com.jme3.bullet.control.GhostControl;

    import com.jme3.bullet.control.RigidBodyControl;

    import com.jme3.collision.CollisionResult;

    import com.jme3.collision.CollisionResults;

    import com.jme3.font.BitmapText;

    import com.jme3.input.KeyInput;

    import com.jme3.input.MouseInput;

    import com.jme3.input.controls.ActionListener;

    import com.jme3.input.controls.KeyTrigger;

    import com.jme3.input.controls.MouseButtonTrigger;

    import com.jme3.light.DirectionalLight;

    import com.jme3.light.PointLight;

    import com.jme3.material.Material;

    import com.jme3.math.ColorRGBA;

    import com.jme3.math.Ray;

    import com.jme3.math.Vector2f;

    import com.jme3.math.Vector3f;

    import com.jme3.renderer.Camera;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.Node;

    import com.jme3.scene.Spatial.CullHint;

    import com.jme3.scene.shape.Sphere;

    import com.jme3.terrain.geomipmap.TerrainLodControl;

    import com.jme3.terrain.heightmap.AbstractHeightMap;

    import com.jme3.terrain.heightmap.ImageBasedHeightMap;

    import com.jme3.terrain.geomipmap.TerrainQuad;

    import com.jme3.texture.Texture;

    import com.jme3.texture.Texture.WrapMode;

    import java.util.ArrayList;

    import java.util.List;



    /**
  • Creates a terrain object and a collision node to go with it. Then
  • drops several balls from the sky that collide with the terrain
  • and roll around.
  • Left click to place a sphere on the ground where the crosshairs intersect the terrain.
  • Hit keys 1 or 2 to raise/lower the terrain at that spot.

    *
  • @author Brent Owens

    */

    public class TerrainTestCollision extends SimpleApplication {



    TerrainQuad terrain;

    Node terrainPhysicsNode;

    Material matRock;

    Material matWire;

    boolean wireframe = false;

    protected BitmapText hintText;

    PointLight pl;

    Geometry lightMdl;

    Geometry collisionMarker;

    private BulletAppState bulletAppState;

    GhostControl controlS;

    Geometry sphere;

    public static void main(String[] args) {

    TerrainTestCollision app = new TerrainTestCollision();

    app.start();

    }



    @Override

    public void initialize() {

    super.initialize();

    loadHintText();

    initCrossHairs();

    }



    @Override

    public void simpleInitApp() {

    bulletAppState = new BulletAppState();

    bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);

    stateManager.attach(bulletAppState);

    setupKeys();

    matRock = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");

    matRock.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));

    Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");

    Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");

    grass.setWrap(WrapMode.Repeat);

    matRock.setTexture("Tex1", grass);

    matRock.setFloat("Tex1Scale", 64f);

    Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");

    dirt.setWrap(WrapMode.Repeat);

    matRock.setTexture("Tex2", dirt);

    matRock.setFloat("Tex2Scale", 32f);

    Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");

    rock.setWrap(WrapMode.Repeat);

    matRock.setTexture("Tex3", rock);

    matRock.setFloat("Tex3Scale", 128f);

    matWire = new Material(assetManager, "Common/MatDefs/Misc/WireColor.j3md");

    matWire.setColor("Color", ColorRGBA.Green);

    AbstractHeightMap heightmap = null;

    try {

    heightmap = new ImageBasedHeightMap(ImageToAwt.convert(heightMapImage.getImage(), false, true, 0), 0.25f);

    heightmap.load();



    } catch (Exception e) { }



    terrain = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap());

    List<Camera> cameras = new ArrayList<Camera>();

    cameras.add(getCamera());

    TerrainLodControl control = new TerrainLodControl(terrain, cameras);

    terrain.addControl(control);

    terrain.setMaterial(matRock);

    terrain.setLocalScale(new Vector3f(2, 2, 2));

    terrain.setModelBound(new BoundingBox());

    terrain.updateModelBound();

    terrain.setLocked(false); // unlock it so we can edit the height

    rootNode.attachChild(terrain);





    /**
  • Create PhysicsRigidBodyControl for collision

    */

    terrain.addControl(new RigidBodyControl(0));

    bulletAppState.getPhysicsSpace().addAll(terrain);





    // Add 5 physics spheres to the world, with random sizes an

    // let them drop from the sky

    float r = 2f;

    controlS = new GhostControl(new SphereCollisionShape®);

    sphere = new Geometry("cannonball",new Sphere(10, 10, r) );

    sphere.setMaterial(matWire);

    sphere.setLocalTranslation(new Vector3f(0, 100 , 0));

    //sphere.addControl(control);

    rootNode.attachChild(sphere);

    bulletAppState.getPhysicsSpace().add(controlS);

    getCamera().lookAt(sphere.getLocalTranslation(), Vector3f.UNIT_Y);



    DirectionalLight dl = new DirectionalLight();

    dl.setDirection(new Vector3f(1, -0.5f, -0.1f).normalizeLocal());

    dl.setColor(new ColorRGBA(0.50f, 0.40f, 0.50f, 1.0f));

    rootNode.addLight(dl);



    getCamera().getLocation().y = 50;;

    }



    public void loadHintText() {

    hintText = new BitmapText(guiFont, false);

    hintText.setSize(guiFont.getCharSet().getRenderedSize());

    hintText.setLocalTranslation(0, getCamera().getHeight(), 0);

    //hintText.setText("Hit T to switch to wireframe");

    hintText.setText("");

    guiNode.attachChild(hintText);

    }



    protected void initCrossHairs() {

    //guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");

    BitmapText ch = new BitmapText(guiFont, false);

    ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);

    ch.setText("+"); // crosshairs

    ch.setLocalTranslation( // center

    settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,

    settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);

    guiNode.attachChild(ch);

    }

    @Override

    public void simpleUpdate(float tpf) {

    sphere.setLocalTranslation(controlS.getPhysicsLocation());

    if (controlS.getOverlappingCount() > 0) {

    controlS.setPhysicsLocation(new Vector3f(0,100,0));

    System.out.println("Touched");

    }

    controlS.setPhysicsLocation(controlS.getPhysicsLocation().subtract(0,-0.000003f,0));

    }



    private void setupKeys() {

    flyCam.setMoveSpeed(50);

    inputManager.addMapping("wireframe", new KeyTrigger(KeyInput.KEY_T));

    inputManager.addListener(actionListener, "wireframe");

    inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_H));

    inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_K));

    inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_U));

    inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_J));

    inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));

    inputManager.addMapping("Reset", new KeyTrigger(KeyInput.KEY_RETURN));

    inputManager.addMapping("Raise", new KeyTrigger(KeyInput.KEY_1));

    inputManager.addMapping("Lower", new KeyTrigger(KeyInput.KEY_2));

    inputManager.addListener(actionListener, "Lefts");

    inputManager.addListener(actionListener, "Rights");

    inputManager.addListener(actionListener, "Ups");

    inputManager.addListener(actionListener, "Downs");

    inputManager.addListener(actionListener, "Space");

    inputManager.addListener(actionListener, "Reset");

    inputManager.addListener(actionListener, "Raise");

    inputManager.addListener(actionListener, "Lower");

    inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));

    inputManager.addListener(actionListener, "shoot");

    }



    @Override

    public void update() {

    super.update();

    }



    private void createCollisionMarker() {

    Sphere s = new Sphere(6, 6, 1);

    collisionMarker = new Geometry("collisionMarker");

    collisionMarker.setMesh(s);

    Material mat = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");

    mat.setColor("Color", ColorRGBA.Orange);

    collisionMarker.setMaterial(mat);

    rootNode.attachChild(collisionMarker);

    }

    private ActionListener actionListener = new ActionListener() {



    public void onAction(String binding, boolean keyPressed, float tpf) {

    if (binding.equals("wireframe") && !keyPressed) {

    wireframe = !wireframe;

    if (!wireframe) {

    terrain.setMaterial(matWire);

    } else {

    terrain.setMaterial(matRock);

    }

    } else if (binding.equals("shoot") && !keyPressed) {



    Vector3f origin = cam.getWorldCoordinates(new Vector2f(settings.getWidth() / 2, settings.getHeight() / 2), 0.0f);

    Vector3f direction = cam.getWorldCoordinates(new Vector2f(settings.getWidth() / 2, settings.getHeight() / 2), 0.3f);

    direction.subtractLocal(origin).normalizeLocal();





    Ray ray = new Ray(origin, direction);

    CollisionResults results = new CollisionResults();

    int numCollisions = terrain.collideWith(ray, results);

    if (numCollisions > 0) {

    CollisionResult hit = results.getClosestCollision();

    if (collisionMarker == null) {

    createCollisionMarker();

    }

    Vector2f loc = new Vector2f(hit.getContactPoint().x, hit.getContactPoint().y);

    System.out.println("collide " + hit.getContactPoint() + ", height: " + terrain.getHeight(loc));

    collisionMarker.setLocalTranslation(hit.getContactPoint());

    }

    } else if (binding.equals("Raise")) {

    if (keyPressed) {

    Vector2f loc = new Vector2f(collisionMarker.getWorldTranslation().x, collisionMarker.getWorldTranslation().z);

    terrain.adjustHeight(loc, 1);

    }

    } else if (binding.equals("Lower")) {

    if (keyPressed) {

    Vector2f loc = new Vector2f(collisionMarker.getWorldTranslation().x, collisionMarker.getWorldTranslation().z);

    terrain.adjustHeight(loc, -1);

    }

    }

    }

    };

    }

    [/java]

If there was something wrong with ghost-terrain collision I guess the normal character wouldnt work too.

Oh, well charactercontrol does work fine. I’ll take look at how CharacterControl works then.

Still… the code works fine when using ie town.zip:

[java]

main.getAssetManager().registerLocator("town.zip", ZipLocator.class.getName());



Spatial sceneModel = main.getAssetManager().loadModel("main.scene");

sceneModel.setLocalScale(2f);



// We set up collision detection for the scene by creating a

// compound collision shape and a static physics node with mass zero.

CollisionShape sceneShape =

CollisionShapeFactory.createMeshShape((Node) sceneModel);

RigidBodyControl landscape = new RigidBodyControl(sceneShape, 0);

sceneModel.addControl(landscape);[/java]

Scrap that. The ghost thinks it collides with the heightest y-point of the level on a point that is not.



Maybe it’s in the way getOverlappingObjects is working?