Help with Physics Tutorial, shooting the cannon

First of all I’d like to say hello to this community, this is my first post. I recently installed JMonkeyEngine and have been doing the tutorials and having lots of fun with it. However, I came up to a problem that I can’t seem to fix :frowning:

I reached the HelloPhysics tutorial but I have been incorporating all the tutorials into one project, not separately like the tutorial does.
My problem is that my cannonball is not shooting straight out into the direction the camera is facing. Seems to be a random direction or something.
When I copy and paste the tutorial code into its own project it works fine, but when it’s incorporated into my project (the one that also includes everything I’ve done in the other tutorials) it doesn’t work correctly (I have modified it to fit my code as best as I think I could).

I wasn’t sure which part of the code is the problem so I just posted the whole thing. Any help is appreciated.

[java]public class HelloJME3 extends SimpleApplication implements AnimEventListener{
public static void main(String[] args){
HelloJME3 app = new HelloJME3();
app.start();
}
private AnimChannel channel;
private AnimControl control;
Node player;
Node shootables;
Geometry mark;
protected Spatial ninja;
protected Geometry box;
protected Geometry shiny_rock;
private float x;
private float y;
private float z;
boolean isRunning = true;
private Material mat1;
private CollisionResult closest;
private Geometry g;
private BulletAppState bulletAppState;
private RigidBodyControl landscape;
private CharacterControl player1;
private Vector3f walkDirection = new Vector3f();
private CollisionResults results;
private boolean left = false,
right = false,
up = false,
down = false;
Material wall_mat;
Material stone_mat;
Material floor_mat;
private RigidBodyControl brick_phy;
private static final Box box1;
private RigidBodyControl ball_phy;
private static final Sphere sphere;
private RigidBodyControl floor_phy;
private static final Box floor;
private static final float brickLength = 0.48f;
private static final float brickWidth = 0.24f;
private static final float brickHeight = 0.12f;

static {
    /** Initialize the cannon ball geometry */
    sphere = new Sphere(32, 32, 0.4f, true, false);
    sphere.setTextureMode(TextureMode.Projected);
    /** Initialize the brick geometry */
    box1 = new Box(Vector3f.ZERO, brickLength, brickHeight, brickWidth);
    box1.scaleTextureCoordinates(new Vector2f(1f, .5f));
    /** Initialize the floor geometry */
    floor = new Box(Vector3f.ZERO, 10f, 0.1f, 5f);
    floor.scaleTextureCoordinates(new Vector2f(3, 6));
}

@Override
public void simpleInitApp(){
    /** Set up Physics */
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
    //bulletAppState.getPhysicsSpace().enableDebug(assetManager);

    flyCam.setMoveSpeed(100);
    viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));
    initKeys();
    initCrossHairs();
    initMark();
    setUpLight();
    initMaterials();
    initWall();
    
    //add Town
    Spatial town = assetManager.loadModel("Scenes/Town/main.j3o");
    town.setLocalTranslation(0, -5.2f, 0);
    town.setLocalScale(2);
    rootNode.attachChild(town);
    
    // We set up collision detection for the player by creating
    // a capsule collision shape and a CharacterControl.
    // The CharacterControl offers extra settings for
    // size, stepheight, jumping, falling, and gravity.
    // We also put the player in its starting position.
    CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 10f, 1);
    player1 = new CharacterControl(capsuleShape, 0.05f);
    player1.setJumpSpeed(20);
    player1.setFallSpeed(30);
    player1.setGravity(50);
    player1.setPhysicsLocation(new Vector3f(0, 5, 5));
    
    //Load controlable model
    player = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml");
    player.setLocalScale(0.5f);
    player.move(-10, 0, -10);
    rootNode.attachChild(player);
    control = player.getControl(AnimControl.class);
    control.addListener(this);
    channel = control.createChannel();
    channel.setAnim("stand");
    
    /** A simple textured cube -- in good MIP map quality. */
    Box boxshape1 = new Box(new Vector3f(-3f,1.1f,0f), 1f,1f,1f);
    Geometry cube = new Geometry("My Textured Box", boxshape1);
    Material mat_stl = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    Texture tex_ml = assetManager.loadTexture("Interface/Logo/Monkey.jpg");
    mat_stl.setTexture("ColorMap", tex_ml);
    cube.setMaterial(mat_stl);
    cube.move(-10, 0, 0);
    rootNode.attachChild(cube);
    
    /** A translucent/transparent texture, similar to a window frame. */
    Box boxshape3 = new Box(new Vector3f(0f,0f,0f), 1f,1f,0.01f);
    Geometry window_frame = new Geometry("window frame", boxshape3);
    Material mat_tt = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat_tt.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png"));
    mat_tt.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
    window_frame.setMaterial(mat_tt);
    //Objects with transparency need to be in the render bucket
    window_frame.setQueueBucket(Bucket.Transparent);
    window_frame.move(-10, 0, 0);
    rootNode.attachChild(window_frame);
    
    /** A cube with base color "leaking" through a partially transparent texture */
    Box boxshape4 = new Box(new Vector3f(3f,-1f,0f), 1f,1f,1f);
    Geometry cube_leak = new Geometry("Leak-through color cube", boxshape4);
    Material mat_tl = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat_tl.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png"));
    mat_tl.setColor("Color", new ColorRGBA(1f,0f,1f, 1f)); // purple
    cube_leak.setMaterial(mat_tl);
    cube_leak.move(-10, 0, 0);
    rootNode.attachChild(cube_leak);
    
    /** A bumpy rock with a shiny light effect */
    Sphere rock = new Sphere(32, 32, 2f);
    shiny_rock = new Geometry("Shiny rock", rock);
    rock.setTextureMode(Sphere.TextureMode.Projected); // better quality on spheres
    TangentBinormalGenerator.generate(rock);           // for lighting effect
    Material mat_lit = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
    mat_lit.setTexture("DiffuseMap", assetManager.loadTexture("Textures/Terrain/Pond/Pond.jpg"));
    mat_lit.setTexture("NormalMap", assetManager.loadTexture("Textures/Terrain/Pond/Pond_normal.png"));
    mat_lit.setBoolean("UseMaterialColors",true);    
    mat_lit.setColor("Specular",ColorRGBA.White);
    mat_lit.setColor("Diffuse",ColorRGBA.White);
    mat_lit.setFloat("Shininess", 5f); // [1,128]    
    shiny_rock.setMaterial(mat_lit);
    shiny_rock.move(10, 0, 0);
    rootNode.attachChild(shiny_rock);
    
    //Pulsating Box
    Box c = new Box(1, 1, 1);
    box = new Geometry("pulsating box", c);
    mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.Red);
    box.setMaterial(mat1);
    box.move(15, 0, 10);
    rootNode.attachChild(box);
    
    //add Teapot
    Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj");
    Material mat_default = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md");
    teapot.setMaterial(mat_default);
    rootNode.attachChild(teapot);
    
    // Create a wall with a simple texture from test_data
    Box boxW = new Box(Vector3f.ZERO, 2.5f,2.5f,1.0f);
    Spatial wall = new Geometry("Box", boxW );
    Material mat_brick = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat_brick.setTexture("ColorMap", 
        assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg"));
    wall.setMaterial(mat_brick);
    wall.setLocalTranslation(2.0f,-2.5f,0.0f);
    rootNode.attachChild(wall);
    
    // Display a line of text with a default font
    guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
    BitmapText helloText = new BitmapText(guiFont, false);
    helloText.setSize(guiFont.getCharSet().getRenderedSize());
    helloText.setText("Hello World");
    helloText.setLocalTranslation(300, helloText.getLineHeight(), 0);
    guiNode.attachChild(helloText);
    
    // Load a model from test_data (OgreXML + material + texture)
    ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");
    ninja.scale(0.04f, 0.04f, 0.04f);
    ninja.rotate(0.0f, -3.0f, 0.0f);
    ninja.setLocalTranslation(0.0f, -5.0f, -2.0f);
    rootNode.attachChild(ninja);
    
    //Create four colored boxes and a floor to shoot at
    shootables = new Node("Shootables");
    rootNode.attachChild(shootables);
    shootables.attachChild(makeCube("a Dragon", -55f, 0f, 0f));
    shootables.attachChild(makeCube("a tin can", -40f, -2f, 15f));
    shootables.attachChild(makeCube("the Sheriff", -50f, 1f, 10f));
    shootables.attachChild(makeCube("the Deputy", -45f, 0f, 25f));
    
    // We set up collision detection for the scene by creating a
    // compound collision shape and a static RigidBodyControl with mass zero.
    CollisionShape sceneShape =
                CollisionShapeFactory.createMeshShape((Node) town);
    BoxCollisionShape wallShape = new BoxCollisionShape(new Vector3f(2.5f, 2.5f, 1f));
    landscape = new RigidBodyControl(sceneShape, 0);
    RigidBodyControl wallPhysics = new RigidBodyControl(wallShape, 0);
    town.addControl(landscape);
    wall.addControl(wallPhysics);
    
    bulletAppState.getPhysicsSpace().add(landscape);
    bulletAppState.getPhysicsSpace().add(player1);
    bulletAppState.getPhysicsSpace().add(wallPhysics);
}

private void setUpLight() {
    // We add light so we see the scene
    AmbientLight al = new AmbientLight();
    al.setColor(ColorRGBA.White.mult(1.3f));
    rootNode.addLight(al);

    DirectionalLight dl = new DirectionalLight();
    dl.setColor(ColorRGBA.White);
    dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal());
    rootNode.addLight(dl);
}

/** A cube object for target practice */
protected Geometry makeCube(String name, float x, float y, float z) {
    Box box = new Box(new Vector3f(x, y, z), 0.1f, 2, 1f);
    Geometry cube = new Geometry(name, box);
    Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.randomColor());
    cube.setMaterial(mat1);
    return cube;
}

/** A red ball that marks the last spot that was "hit" by the "shot". */
protected void initMark() {
    Sphere sphere = new Sphere(30, 30, 0.2f);
    mark = new Geometry("BOOM!", sphere);
    Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mark_mat.setColor("Color", ColorRGBA.Red);
    mark.setMaterial(mark_mat);
}

/** A centred plus sign to help the player aim. */
protected void initCrossHairs() {
    guiNode.detachAllChildren();
    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){
    Vector3f camDir = cam.getDirection().clone().multLocal(0.3f);
    Vector3f camLeft = cam.getLeft().clone().multLocal(0.2f);
    walkDirection.set(0, 0, 0);
    if (left)  { walkDirection.addLocal(camLeft); }
    if (right) { walkDirection.addLocal(camLeft.negate()); }
    if (up)    { walkDirection.addLocal(camDir); }
    if (down)  { walkDirection.addLocal(camDir.negate()); }
    player1.setWalkDirection(walkDirection);
    cam.setLocation(player1.getPhysicsLocation());
    
    z += tpf;
    if (z < 5){
        shiny_rock.rotate(2*tpf, 0, 0);
        shiny_rock.move(0, 0, 3*tpf);
    }
    else if (z < 10){
        shiny_rock.rotate(-2*tpf, 0, 0);
        shiny_rock.move(0, 0, -3*tpf);
    }
    else
        z = 0;
    
    box.rotate(0, 4*tpf, 0);
    x += tpf;
    if (x < 2)
        box.scale(1 + tpf * 0.4f);
    else if (x < 4)
        box.scale(1 - tpf * 0.4f);
    else
        x = 0;
    
    y += tpf;
    if (y > 1) {
        mat1.setColor("Color", ColorRGBA.randomColor());
        y= 0;
    }
}

public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
    if (animName.equals("Walk")) {
      channel.setAnim("stand", 0.50f);
      channel.setLoopMode(LoopMode.DontLoop);
      channel.setSpeed(1f);
    }
}

public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
    // unused
}

private void initKeys(){
    //map the keys to the desired name
    inputManager.addMapping("Pause", new KeyTrigger(KeyInput.KEY_P));
    inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A));
    inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D));
    inputManager.addMapping("Jump", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addMapping("Forward", new KeyTrigger(KeyInput.KEY_W));
    inputManager.addMapping("Back", new KeyTrigger(KeyInput.KEY_S));
    inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_U));
    inputManager.addMapping("Shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    
    // Add the names to the action listener.
    inputManager.addListener(myListener, new String[]{"Pause", "Left", "Right",
                                                        "Walk", "Jump",
                                                        "Forward", "Back", "Shoot"});
}
private Listener myListener = new Listener();

private  class Listener implements ActionListener, AnalogListener{
    public void onAction(String name, boolean value, float tpf){
        if (name.equals("Left")){
            //isRunning = !isRunning;
            left = value;
        } else if (name.equals("Right")){
            right = value;
        } else if (name.equals("Forward")){
            up = value;
        } else if (name.equals("Back")){
            down = value;
        } else if (name.equals("Jump")){
            player1.jump();
        }
        
        if (name.equals("Walk")){
            channel.setAnim("Walk", 0.50f);
            channel.setLoopMode(LoopMode.Loop);
        }
        if (name.equals("Shoot") && !value) {
            makeCannonBall();
        }
    }

    public void onAnalog(String name, float value, float tpf) {
        
    }
}
    /** Initialize the materials used in this scene. */
public void initMaterials() {
    wall_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    TextureKey key = new TextureKey("Textures/Terrain/BrickWall/BrickWall.jpg");
    key.setGenerateMips(true);
    Texture tex = assetManager.loadTexture(key);
    wall_mat.setTexture("ColorMap", tex);

    stone_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
    key2.setGenerateMips(true);
    Texture tex2 = assetManager.loadTexture(key2);
    stone_mat.setTexture("ColorMap", tex2);

    floor_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.jpg");
    key3.setGenerateMips(true);
    Texture tex3 = assetManager.loadTexture(key3);
    tex3.setWrap(WrapMode.Repeat);
    floor_mat.setTexture("ColorMap", tex3);
}

/** This loop builds a wall out of individual bricks. */
public void initWall() {
    float startpt = brickLength / 4;
    float height = 0;
    for (int j = 0; j < 15; j++) {
        for (int i = 0; i < 6; i++) {
            Vector3f vt =
             new Vector3f(i * brickLength * 2 + startpt, brickHeight + height, 0);
            makeBrick(vt);
        }
      startpt = -startpt;
      height += 2 * brickHeight;
    }
}

/** This method creates one individual physical brick. */
public void makeBrick(Vector3f loc) {
    /** Create a brick geometry and attach to scene graph. */
    Geometry brick_geo = new Geometry("brick", box1);
    brick_geo.setMaterial(wall_mat);
    rootNode.attachChild(brick_geo);
    /** Position the brick geometry  */
    brick_geo.setLocalTranslation(loc);
    /** Make brick physical with a mass > 0.0f. */
    brick_phy = new RigidBodyControl(2f);
    /** Add physical brick to physics space. */
    brick_geo.addControl(brick_phy);
    bulletAppState.getPhysicsSpace().add(brick_phy);
}

/** This method creates one individual physical cannon ball.
* By defaul, the ball is accelerated and flies
* from the camera position in the camera direction.*/
public void makeCannonBall() {
    /** Create a cannon ball geometry and attach to scene graph. */
    Geometry ball_geo = new Geometry("cannon ball", sphere);
    ball_geo.setMaterial(stone_mat);
    rootNode.attachChild(ball_geo);
    /** Position the cannon ball  */
    ball_geo.setLocalTranslation(cam.getLocation());
    /** Make the ball physcial with a mass > 0.0f */
    ball_phy = new RigidBodyControl(1f);
    /** Add physical ball to physics space. */
    ball_geo.addControl(ball_phy);
    bulletAppState.getPhysicsSpace().add(ball_phy);
    /** Accelerate the physcial ball to shoot it. */
    ball_phy.setLinearVelocity(cam.getDirection().mult(25));
}

} [/java]

The code provided is not runnable without its import statements and the “Scenes/Town/main.j3o” model.

The problem might be that the camera is getting updated twice, once by flyCam and once by cam.setLocation() in simpleUpdate(). But it’s hard to tell without actually running your code. What happens if you comment out the cam.setLocation() call?

I think maybe what could be happening is, because your character control has a rigidbody shape, the ball is spawning in the exact same location as the character and so they are both knocked back a little bit. That’s why you will see the player knocked back a bit as well as the ball moving in an unpredictable direction.

The code is runnable, I had to use my own scene, fix imports, and find and replace the different ascii versions of speech marks and - signs that the code tags seem to put in, not OP’s fault. However, OP, are you using the latest version of JME? There are a few other things I had to fix too and it seemed like our versions might be different. I’ll check if I’m up to date now.

Edit: yeh, sorry about that I’ve just noticed I wasn’t quite up to date :confused:

thanks for the replies.

sgold - commenting out cam.setLocation(player1.getPhysicsLocation()); does indeed fix the ball shooting problem, but now the camera is back to free control and not attached to the rigid capsule, which is where I want it.

Dan - What you are experience is precisely what happens to me, and I do believe my JME is up to date as I just recently updated it. But what you mentioned about the ball and ‘player’ spawning in the same place seems to be the problem based on the test sgold suggested.

So based on what both of you have said, would a fix possibly be to simply spawn the cannonball outside of the rigid capsule(player)? If so, I’m not sure I know how to do that as I can’t simply change the z coordinate since it’s dependent on where the camera is looking. How do I offset straight forward from the camera direction? I’m sorry I’m still a little noobish.

EDIT: I changed “ball_geo.setLocalTranslation(cam.getLocation());” to “ball_geo.setLocalTranslation(cam.getLocation().add(cam.getDirection().mult(2)));” and it seems to have fixed the problem, but is that a good way of doing it or is it a silly fix?

A good solution might be to disable flycam and set the camera orientation based on the player1 orientation.

sgold, wouldn’t the ball still spawn in the same position as the player’s capsule though, as cam location = player location?

Faris, that seems like it should work to me, I guess if it works then it’s fine

@javagame said: sgold, wouldn't the ball still spawn in the same position as the player's capsule though, as cam location = player location?

Faris, that seems like it should work to me, I guess if it works then it’s fine

@javagame, the ball would still spawn inside the capsule. If that’s the issue then Faris’s tweak to the ball’s start location should resolve it.

Alright thanks guys.

On a side note I noticed that if I increase “ball_phy.setLinearVelocity(cam.getDirection().mult(25));” to anything higher than .mult(50) then some balls start clipping through the world as if there was no rigid body at all, and it gets worse the higher i go (I was trying to simulate a real cannon being fired at very high speeds like say at .mult(500)).

Anybody know why this happens or if there’s a fix? I’d like to think that the computer can create the rigid bodies fast enough but I don’t know

@Spayd said: ...(I was trying to simulate a real cannon being fired at very high speeds like say at .mult(500)).

Anybody know why this happens or if there’s a fix? I’d like to think that the computer can create the rigid bodies fast enough but I don’t know

It happens because the object is moving to fast, one simulation tick it is in front of the object, next simulation tick it has moved behind the object so no collision is noticed. Bullet has something called CCD that can help. Search for CCD in for example: https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:advanced:physics

There are physics settings setCcdMotionThreshold() and setCcdSweptSphereRadius() that you can apply to fast-moving objects (like cannonballs) to prevent them from passing through other objects undetected. I use these in my own game.

The documentation for CCD is a bit confusing:

http://www.bulletphysics.org/mediawiki-1.5.8/index.php?title=Anti_tunneling_by_Motion_Clamping
https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:advanced:physics#physicsspace_code_samples
http://hub.jmonkeyengine.org/javadoc/com/jme3/bullet/objects/PhysicsRigidBody.html

Contrary to what the JME3 wiki implies, these are per-object parameters, not per-space parameters.

If your physics is running at 100 updates per second and you have a cannon ball that’s 0.1 world unit in diameter travelling at 200 wu/sec and you want to make sure it notices a wall that’s 0.2 world units thick, then I believe the appropriate settings would be:

rigidBodyControl.setCcdMotionThreshold(0.5f*200f/100f);
rigidBodyControl.setCcdSweptSphereRadius(0.1f);

1 Like

The cannon ball is moving fast enough that it effectively teleports past the object.