JME 3 Tutorial (13) - Hello Physics

Previous: Hello Effects, Next: JME 3 documentation

For the simulation of physical forces, jME3 integrates the jBullet library. The most common use cases for physics in 3D games are:

All that can be done in JME3. Let's have a look at a Physics simulation in this example where we shoot cannon balls at a wall.

Sample Code

Thanks to double1984 for contributing this fun sample!

package jme3test.helloworld;
 
import com.jme3.app.SimpleApplication;
import com.jme3.asset.TextureKey;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.font.BitmapText;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.scene.shape.Sphere.TextureMode;
import com.jme3.shadow.BasicShadowRenderer;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
 
/**
 * Example 12 - how to give objects physical properties so they bounce and fall.
 * @author base code by double1984, updated by zathras
 */
public class HelloPhysics extends SimpleApplication {
 
  public static void main(String args[]) {
    HelloPhysics app = new HelloPhysics();
    app.start();
  }
 
  /** Prepare the Physics Application State (jBullet) */
  private BulletAppState bulletAppState;
 
  /** Activate custom rendering of shadows */
  BasicShadowRenderer bsr;
 
  /** Prepare Materials */
  Material wall_mat;
  Material stone_mat;
  Material floor_mat;
 
  /** Prepare geometries and physical nodes for bricks and cannon balls. */
  private RigidBodyControl    brick_phy;
  private static final Box    box;
  private RigidBodyControl    ball_phy;
  private static final Sphere sphere;
  private RigidBodyControl    floor_phy;
  private static final Box    floor;
 
  /** dimensions used for bricks and wall */
  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 */
    box = new Box(Vector3f.ZERO, brickLength, brickHeight, brickWidth);
    box.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 Game */
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
    /** Configure cam to look at scene */
    cam.setLocation(new Vector3f(0, 6f, 6f));
    cam.lookAt(Vector3f.ZERO, new Vector3f(0, 1, 0));
    cam.setFrustumFar(15);
    /** Add InputManager action: Left click triggers shooting. */
    inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    inputManager.addListener(actionListener, "shoot");
    /** Initialize the scene, materials, and physics space */
    initMaterials();
    initWall();
    initFloor();
    initCrossHairs();
    initShadows();
  }
 
  /**
   * Every time the shoot action is triggered, a new cannon ball is produced.
   * The ball is set up to fly from the camera position in the camera direction.
   */
  private ActionListener() {
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("shoot") && !keyPressed) {
        makeCannonBall();
      }
    }
  };
 
  /** 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.png");
    key3.setGenerateMips(true);
    Texture tex3 = assetManager.loadTexture(key3);
    tex3.setWrap(WrapMode.Repeat);
    floor_mat.setTexture("ColorMap", tex3);
  }
 
  /** Make a solid floor and add it to the scene. */
  public void initFloor() {
    Geometry floor_geo = new Geometry("Floor", floor);
    floor_geo.setMaterial(floor_mat);
    floor_geo.setShadowMode(ShadowMode.Receive);
    floor_geo.setLocalTranslation(0, -0.1f, 0);
    this.rootNode.attachChild(floor_geo);
    /* Make the floor physical with mass 0.0f! */
    floor_phy = new RigidBodyControl(0.0f);
    floor_geo.addControl(floor_phy);
    bulletAppState.getPhysicsSpace().add(floor_phy);
  }
 
  /** 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 < 4; i++) {
        Vector3f vt =
         new Vector3f(i * brickLength * 2 + startpt, brickHeight + height, 0);
        makeBrick(vt);
      }
      startpt = -startpt;
      height += 2 * brickHeight;
    }
  }
 
  /** Activate shadow casting and light direction */
  private void initShadows() {
    bsr = new BasicShadowRenderer(assetManager, 256);
    bsr.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
    viewPort.addProcessor(bsr);
    // Default mode is Off -- Every node declares own shadow mode!
    rootNode.setShadowMode(ShadowMode.Off);
  }
 
  /** 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", box);
    brick_geo.setMaterial(wall_mat);
    rootNode.attachChild(brick_geo);
    /** Position the brick geometry and activate shadows */
    brick_geo.setLocalTranslation(loc);
    brick_geo.setShadowMode(ShadowMode.CastAndReceive);
    /** 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 and activate shadows */
    ball_geo.setLocalTranslation(cam.getLocation());
    ball_geo.setShadowMode(ShadowMode.CastAndReceive);
    /** 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));
  }
 
  /** A plus sign used as crosshairs to help the player with aiming.*/
  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("+");        // fake crosshairs :)
    ch.setLocalTranslation( // center
      settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
      settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
    guiNode.attachChild(ch);
  }
}

You should see a brick wall that is casting a shadow on a floor. Click to shoot cannon balls. Watch the bricks fall and bounce off one another!

A Basic Physics Application

In the previous tutorials, we were using Geometries (boxes, spheres, and models) that we placed in the scene. Geometries can float in mid-air and even overlap – they are not affected by "gravity" and have no physical mass. This tutorial shows how to add physical properties to Geometries.

As always, we start with a standard com.jme3.app.SimpleApplication. To activate physics, we create a com.jme3.bullet.BulletAppState, and and attach it to the SimpleApplication's application state manager.

public class HelloPhysics extends SimpleApplication {
  private BulletAppState bulletAppState;
 
  public void simpleInitApp() {
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
  }
}

The BulletAppState gives the game access to a Physics Space. The Physics Space allows us to use com.jme3.bullet.control.PhysicsControls that add physical properties to Nodes.

Creating Bricks and Cannon Balls

Geometries

In this "shoot at the wall" example, we use Geometries such as cannon balls and bricks. A geometry just describes the shape and look of an object.

  /** Prepare geometries and physical nodes for bricks and cannon balls. */
  private static final Box    box;
  private static final Sphere sphere;
  private static final Box    floor;
 
  /** dimensions used for bricks and wall */
  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 */
    box = new Box(Vector3f.ZERO, brickLength, brickHeight, brickWidth);
    box.scaleTextureCoordinates(new Vector2f(1f, .5f));
    /** Initialize the floor geometry */
    floor = new Box(Vector3f.ZERO, 10f, 0.1f, 5f);
    floor.scaleTextureCoordinates(new Vector2f(3, 6));
  }

RigidBodyControl: Brick

For each Geometry that we want to have physcial properties, we add a RigidBodyControl.

  private RigidBodyControl brick_phy;

The makeBrick(loc) methods creates a physics node brickNode at a location loc. Our brick shall have

  public void makeBrick(Vector3f loc) {
    /** Create a brick geometry and attach to scene graph. */
    Geometry brick_geo = new Geometry("brick", box);
    brick_geo.setMaterial(wall_mat);
    rootNode.attachChild(brick_geo);
    /** Position the brick geometry and activate shadows */
    brick_geo.setLocalTranslation(loc);
    brick_geo.setShadowMode(ShadowMode.CastAndReceive);
    /** 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 code sample does the following:

  1. We use a box shape as brick, and give it a brick-colored material.
  2. We attach the brick to the rootNode and position it at the position loc in the scene graph.
  3. (Optionally, we activate a "Cast and Receive" shadow mode for each brick.)
  4. We create a RigidBodyControl for the brick, add it to the brick Geometry, and register it to the PhysicsSpace.

RigidBodyControl: Cannonball

You will notice that the cannon ball is created in the same way:

The makeCannonBall() methods creates a physics node cannonballNode. The cannon ball shall have

  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 and activate shadows */
    ball_geo.setLocalTranslation(cam.getLocation());
    ball_geo.setShadowMode(ShadowMode.CastAndReceive);
    /** 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));
  }

This code sample does the following:

  1. We use a sphere shape as cannonball, and give it a stone material.
  2. We attach the ball to the rootNode and position it where the camera is.
  3. (Optionally, we activate a "Cast and Receive" shadow mode for the ball.)
  4. We create a RigidBodyControl for the ball, add it to the ball Geometry, and register it to the PhysicsSpace.
  5. Since are are shooting cannon balls here, we accelerate the ball in the direction the camera is looking, with a speed of 25f.

RigidBodyControl: Floor

The (static) floor has one important difference compared to the (dynamic) bricks and cannonballs: A mass of zero.

As before, we write a custom initFloor() method that creates a flat box with a rock texture that we use as floor. The floor shall have:

  private RigidBodyControl    floor_phy;
  ...
  public void initFloor() {
    Box(Vector3f.ZERO, 10f, 0.1f, 5f);
    floorBox.scaleTextureCoordinates(new Vector2f(3, 6));
    floor_geo = new Geometry("floor", floorBox);
    floor_geo.setMaterial(floor_mat);
    floor_geo.setShadowMode(ShadowMode.Receive);
    floor_geo.setLocalTranslation(0, -0.1f, 0);
    this.rootNode.attachChild(floor_geo);
    /* Make the floor physical with mass 0.0f! */
    floor_phy = new RigidBodyControl(0.0f);
    floor_geo.addControl(floor_phy);
    bulletAppState.getPhysicsSpace().add(floor_phy);
 

This code sample does the following:

  1. We use a box shape as floor, and give it a floor material.
  2. We attach the floor to the rootNode and position it a bit below the origin – to prevent overlap with other physical nodes.
  3. (Optionally, we activate a "Receive" shadow mode for the floor. The floor does not cast any shadows, this saves computing time.)
  4. Static objects such as floors are mass-less and are not affected by gravity! Therefor we create a RigidBodyControl with a mass of 0.0f.
  5. We add the RigidBodyControl to the floor Geometry, and register it to the PhysicsSpace.

Creating the Scene

Let's have a quick look at the remaining custom helper methods: initMaterial(), initShadows(), initCrossHairs(), and initWall().

These methods are each called once from the simpleInitApp() method at the start of the game. As you see, you write any number of custom methods to set up your game's scene.

The Cannon Ball Shooting Action

In the initSimpleApp() we add an input mapping that triggers a shoot action when the left mouse button is pressed.

    public void simpleInitApp() {
    ...
        inputManager.addMapping("shoot", new MouseButtonTrigger(0));
        inputManager.addListener(actionListener, "shoot");
    ...
    }

The action of shooting a new cannon ball is defined as follows:

    private ActionListener() {
        public void onAction(String name, boolean keyPressed, float tpf) {
            if (name.equals("shoot") && !keyPressed) {
                makeCannonBall();
            }
        }
    };

In the moment the cannonball appears in the scene, it flies off with the velocity (and in the direction) that we have specified using setLinearVelocity() inside makeCannonBall(). The newly created cannon ball flies off, hits the wall, and exerts a physical force that shifts the individual bricks.

Movement of the physics enabled Spatial

The location of the spatial is defined by the RigidBodyControl, move that to move the spatial or if its a non-world-object set the RigidBodyControl to kinematic mode to have it move along with the spatial. This will make the RigidBody be unaffected by the physics but it will effect the physics objects around it based on its location and amount of movement that is applied. Note that a kinematic RigidBody needs to have a mass!

Excercises

Exercise 1

What happens if you give a static node such as the floor a mass of more than 0.0f?

Exercise 2

Popular AAA games use a clever mix of physics, animation and prerendered graphics to give you the illusion of a real, "physical" world. Look at your favorite games and try to spot where and how the game designers trick you into believing that the whole scene is physical. – For example, a building "breaking" into 4-8 parts when falling apart is most likely being replaced by dynamic physics nodes only after it has been destroyed… Now that you start to implement game physics yourself, look behind the curtain.

Conclusion

Using physics everywhere in a game sounds like a cool idea, but it is easily overused. Although the physics nodes are put to "sleep" when they are not moved, creating a world solely out of dynamic physics nodes will quickly bring you to the limits of your computer's capabilities.

You have learned how to add a PhysicsSpace to an application by attaching a BulletAppState. You know how to create PhysicsNodes from a geometry, a collision shape, and a mass value. You have learned that physical objects are not only attached to the rootNode but also registered to the PhysicsSpace. You are aware that overusing physics has a huge performance impact.

Additionally you have learned how to add shadows to a scene.


This is the last beginner tutorial for now. Now you are ready to start combining what you learned to create a game of your own!

beginner, intro, physics, documentation, input, model, controller

view online version