Physics with Bullet

jMonkeyEngine 3.0 Cookbook


August 2014

$23.99

Over 80 practical recipes to expand and enrich your jMonkeyEngine skill set with a close focus on game development.

In this article by Rickard Eden author of jMonkeyEngine 3.0 CookBook we will learn how to use physics in games using different physics engine. This article contains the following recipes:

  • Creating a pushable door
  • Building a rocket engine
  • Ballistic projectiles and arrows
  • Handling multiple gravity sources
  • Self-balancing using RotationalLimitMotors

(For more resources related to this topic, see here.)

Using physics in games has become very common and accessible, thanks to open source physics engines, such as Bullet. jMonkeyEngine supports both the Java-based jBullet and native Bullet in a seamless manner.

jBullet is a Java-based library with JNI bindings to the original Bullet based on C++. jMonkeyEngine is supplied with both of these, and they can be used interchangeably by replacing the libraries in the classpath. No coding change is required. Use jme3-libraries-physics for the implementation of jBullet and jme3-libraries-physics-native for Bullet. In general, Bullet is considered to be faster and is full featured.

Physics can be used for almost anything in games, from tin cans that can be kicked around to character animation systems. In this article, we'll try to reflect the diversity of these implementations.

Creating a pushable door

Doors are useful in games. Visually, it is more appealing to not have holes in the walls but doors for the players to pass through. Doors can be used to obscure the view and hide what's behind them for a surprise later. In extension, they can also be used to dynamically hide geometries and increase the performance. There is also a gameplay aspect where doors are used to open new areas to the player and give a sense of progression.

In this recipe, we will create a door that can be opened by pushing it, using a HingeJoint class.

This door consists of the following three elements:

  • Door object: This is a visible object
  • Attachment: This is the fixed end of the joint around which the hinge swings
  • Hinge: This defines how the door should move

Getting ready

Simply following the steps in this recipe won't give us anything testable. Since the camera has no physics, the door will just sit there and we will have no way to push it. If you have made any of the recipes that use the BetterCharacterControl class, we will already have a suitable test bed for the door. If not, jMonkeyEngine's TestBetterCharacter example can also be used.

How to do it...

This recipe consists of two sections. The first will deal with the actual creation of the door and the functionality to open it. This will be made in the following six steps:

  1. Create a new RigidBodyControl object called attachment with a small BoxCollisionShape. The CollisionShape should normally be placed inside the wall where the player can't run into it. It should have a mass of 0, to prevent it from being affected by gravity.
  2. We move it some distance away and add it to the physicsSpace instance, as shown in the following code snippet:

    attachment.setPhysicsLocation(new Vector3f(-5f, 1.52f, 0f)); bulletAppState.getPhysicsSpace().add(attachment);

  3. Now, create a Geometry class called doorGeometry with a Box shape with dimensions that are suitable for a door, as follows:

    Geometry doorGeometry = new Geometry("Door", new Box(0.6f, 1.5f, 0.1f));

  4. Similarly, create a RigidBodyControl instance with the same dimensions, that is, 1 in mass; add it as a control to the doorGeometry class first and then add it to physicsSpace of bulletAppState. The following code snippet shows you how to do this:

    RigidBodyControl doorPhysicsBody = new RigidBodyControl(new BoxCollisionShape(new Vector3f(.6f, 1.5f, .1f)), 1);
    bulletAppState.getPhysicsSpace().add(doorPhysicsBody); doorGeometry.addControl(doorPhysicsBody);

  5. Now, we're going to connect the two with HingeJoint. Create a new HingeJoint instance called joint, as follows:

    new HingeJoint(attachment, doorPhysicsBody, new Vector3f(0f, 0f, 0f),
    new Vector3f(-1f, 0f, 0f), Vector3f.UNIT_Y, Vector3f.UNIT_Y);

  6. Then, we set the limit for the rotation of the door and add it to physicsSpace as follows:

    joint.setLimit(-FastMath.HALF_PI - 0.1f, FastMath.HALF_PI + 0.1f);
    bulletAppState.getPhysicsSpace().add(joint);

Now, we have a door that can be opened by walking into it. It is primitive but effective. Normally, you want doors in games to close after a while. However, here, once it is opened, it remains opened. In order to implement an automatic closing mechanism, perform the following steps:

  1. Create a new class called DoorCloseControl extending AbstractControl.
  2. Add a HingeJoint field called joint along with a setter for it and a float variable called timeOpen.
  3. In the controlUpdate method, we get hingeAngle from HingeJoint and store it in a float variable called angle, as follows:

    float angle = joint.getHingeAngle();

  4. If the angle deviates a bit more from zero, we should increase timeOpen using tpf. Otherwise, timeOpen should be reset to 0, as shown in the following code snippet:

    if(angle > 0.1f || angle < -0.1f) timeOpen += tpf; else timeOpen = 0f;

  5. If timeOpen is more than 5, we begin by checking whether the door is still open. If it is, we define a speed to be the inverse of the angle and enable the door's motor to make it move in the opposite direction of its angle, as follows:

    if(timeOpen > 5) { float speed = angle > 0 ? -0.9f : 0.9f; joint.enableMotor(true, speed, 0.1f);
    spatial.getControl(RigidBodyControl.class).activate(); }

  6. If timeOpen is less than 5, we should set the speed of the motor to 0:

    joint.enableMotor(true, 0, 1);

  7. Now, we can create a new DoorCloseControl instance in the main class, attach it to the doorGeometry class, and give it the same joint we used previously in the recipe, as follows:

    DoorCloseControl doorControl = new DoorCloseControl(); doorControl.setHingeJoint(joint);
    doorGeometry.addControl(doorControl);

How it works...

The attachment RigidBodyControl has no mass and will thus not be affected by external forces such as gravity. This means it will stick to its place in the world. The door, however, has mass and would fall to the ground if the attachment didn't keep it up with it.

The HingeJoint class connects the two and defines how they should move in relation to each other. Using Vector3f.UNIT_Y means the rotation will be around the y axis. We set the limit of the joint to be a little more than half PI in each direction. This means it will open almost 100 degrees to either side, allowing the player to step through.

When we try this out, there may be some flickering as the camera passes through the door. To get around this, there are some tweaks that can be applied. We can change the collision shape of the player. Making the collision shape bigger will result in the player hitting the wall before the camera gets close enough to clip through. This has to be done considering other constraints in the physics world.

You can consider changing the near clip distance of the camera. Decreasing it will allow things to get closer to the camera before they are clipped through. This might have implications on the camera's projection.

One thing that will not work is making the door thicker, since the triangles on the side closest to the player are the ones that are clipped through. Making the door thicker will move them even closer to the player.

In DoorCloseControl, we consider the door to be open if hingeAngle deviates a bit more from 0. We don't use 0 because we can't control the exact rotation of the joint. Instead we use a rotational force to move it. This is what we do with joint.enableMotor. Once the door is open for more than five seconds, we tell it to move in the opposite direction. When it's close to 0, we set the desired movement speed to 0. Simply turning off the motor, in this case, will cause the door to keep moving until it is stopped by an external force.

Once we enable the motor, we also need to call activate() on RigidBodyControl or it will not move.

Building a rocket engine

A rocket engine is crucial for most space-based games and many 2D games as well. In this recipe, we'll cover the minimum that is required to create a thruster that can be used in many different contexts. The following figure shows a thruster with ParticleEmitter:

Getting ready

For this recipe, we need to make sure that we see the debug shapes of physics. To do this, we need to call the bulletAppState.setDebugEnabled(true); statement.

How to do it...

We will begin by setting up some things that are not strictly needed for the rocket engine but will aid the testing. Perform the following steps to build a rocket engine:

  1. First of all we add a floor mesh. For this, we create a new Node class called ground.
  2. To do this, we add RigidBodyControl with PlaneCollisionShape. The plane should face upwards like floors normally do, as follows:

    RigidBodyControl floorControl = new RigidBodyControl(new PlaneCollisionShape(new Plane(new Vector3f(0, 1, 0), 0)), 0);
    ground.addControl(floorControl); floorControl.setPhysicsLocation(new Vector3f(0f, -10, 0f));

  3. We then attach them both to rootNode of the application and physicsSpace of bulletAppState.
  4. Finally, we need to add a key to control the booster. For this, we implement an AnalogListener interface in our application.
  5. Then, add the application to inputManager along with a mapping object called boost that is bound to the Space bar:

    inputManager.addListener(this, "boost"); inputManager.addMapping("boost", new KeyTrigger(KeyInput.KEY_SPACE));

  6. Most of this recipe will be implemented in a class that extends SimpleApplication.
  7. We begin by defining a Node class called spaceShip that will be our spaceship's representation.
  8. We then create a RigidBodyControl instance with BoxCollisionShape and add it to the spaceShip node as follows:

    RigidBodyControl control = new RigidBodyControl(new BoxCollisionShape(new Vector3f(1, 1, 1)), 1);
    spaceShip.addControl(control);

  9. Now, we create another Node, which will be our thruster. Give it the name Thruster to be able to identify it more easily later, as follows:

    Node thruster = new Node("Thruster");

  10. We set localTranslation of this so that it will end up at the bottom of the spaceship, as shown in the following line of code:

    thruster.setLocalTranslation(0, -1, 0);

  11. Then, we attach it to the spaceShip node.
  12. Now, we have to attach the spaceShip node to both the rootNode and physicsSpace of bulletAppState.
  13. To control the thruster and make it more reusable, we will create a class called ThrusterControl, extending AbstractControl.
  14. It'll have one field, a Spatial field called thruster, that will store the thruster node.
  15. We will override the setSpatial method and set it by calling getChild("Thruster") on the supplied spatial.
  16. Lastly, we define a new method called fireBooster().
  17. Inside this, we subtract the thruster's location from the spaceship's location and store it in a new Vector3f field called direction as follows:

    Vector3f direction = spatial.getWorldTranslation().subtract(thruster.getWorldTra nslation());

  18. Then, we find the RigidBodyControl class in the spatial and call applyImpulse with the direction vector. We use the inverted direction as the relative position that the impulse should originate from. This can be implemented as follows:

    spatial.getControl(RigidBodyControl.class).applyImpulse(dir ection, direction.negate());

  19. In the application class, we have to make it call the fireBooster method. We do this in the onAnalog method that was added when we implemented the AnalogListener interface:

    if(name.equals("boost") && value > 0){ spaceShip.getControl(ThrusterControl.class).fireBooster(); }

How it works...

The graphics in this recipe are very minimalistic and mostly rely on the debug mode of BulletAppState to draw them. The physics shapes don't normally have a visual representation since they're not part of the scene graph. Using the debug mode can be very useful during early prototypes.

The RigidBodyControl instance of the spaceship makes sure it's affected by gravity and other forces.

The sole purpose of a thruster is to be able to easily retrieve the position that is relative to the spaceship from where the boosting force needs to be applied. This is why we place it at the bottom of the spaceship. The benefit of using the Control pattern to control a Thruster is that we can apply it to other geometries easily (and even use it in SceneComposer). The fireBooster method of ThrusterControl takes the position of spaceShip and subtracts the position of the thruster node to get the direction of the force to apply. The relative position of the force is the direct opposite of this direction.

Ballistic projectiles and arrows

Applying physics to arrows can greatly improve the appearance and gameplay of a medieval or fantasy game. Setting up arrows that are affected by gravity is fairly simple; this recipe, however, will also set the arrows up in a way that they always face the direction they're traveling in, making them more realistic. The following figure shows one of the arrows in flight:

Getting ready

For this recipe, we need to make sure that we see the debug shapes of physics. To do this, we need to call the bulletAppState.setDebugEnabled(true); statement.

How to do it...

In this recipe, we'll create three classes. Let's begin by looking at the Arrow class, which contains most of the new functionalities. This will be done in the following eight steps:

  1. We create a new class called Arrow, extending Node.
  2. Its constructor takes two Vector3f variables as parameters. One of these is for the starting location of the arrow and one for the initial velocity, as shown in the following line of code:

    public Arrow(Vector3f location, Vector3f velocity)

  3. Inside the constructor, we define a Geometry instance for the body of the arrow with a box mesh as follows:

    Box arrowBody = new Box(0.3f, 4f, 0.3f); Geometry geometry = new Geometry("bullet", arrowBody);

  4. Then, we set localTranslation of Geometry so that one of its ends touches the center point of the node as follows:

    geometry.setLocalTranslation(0f, -4f, 0f);

  5. We set localTranslation of this Arrow as the supplied location.
  6. Next, we create CollisionShape. This will represent the head of the arrow and can be SphereCollisionShape, as follows:

    SphereCollisionShape arrowHeadCollision = new SphereCollisionShape(0.5f);

  7. Now, we define RigidBodyControl based on CollisionShape, as follows:

    RigidBodyControl rigidBody = new RigidBodyControl(arrowHeadCollision, 1f);

  8. We set LinearVelocity of RigidBodyControl to be the supplied velocity and add it as a Control to Arrow, as follows:

    rigidBody.setLinearVelocity(velocity); addControl(rigidBody);

This would be enough for the arrow to follow the laws of physics; however, it will always face the forward direction. By adding another control, we can make it face the direction of the velocity. To do this, perform the following steps:

  1. Create another class called ArrowFacingControl, extending AbstractControl.
  2. We add a Vector3f field called direction.
  3. In the controlUpdate method, we get linearVelocity from RigidBodyControl of the spatial and normalize it. We then store it in direction as follows:

    direction = spatial.getControl(RigidBodyControl.class).getLinearVelocit y().normalize();

  4. Then, call the spatial and tell it to rotate to the supplied direction vector as follows:

    spatial.rotateUpTo(direction);

  5. In the constructor of the Arrow class, we add an instance of this control, as follows:

    addControl(new ArrowFacingControl());

The last section handles the firing of the arrow from SimpleApplication. This can be done with the following steps:

  1. First of all, we need to implement ActionListener in the application.
  2. Add the ActionListener class to inputManager as a listener, together with a key for firing arrows, as follows:

    inputManager.addListener(this, "fire"); inputManager.addMapping("fire", new KeyTrigger(KeyInput.KEY_SPACE));

  3. In the onAction method, call a new method called fireArrow when the fire button is released. This can be implemented as follows:

    if (action.equals("fire") && !isPressed) fireArrow();

  4. The fireArrow method should begin by instancing a new Arrow instance and applying a (preloaded) material to it, as follows:

    Arrow arrow = new Arrow(new Vector3f(0f, 6f, -10f), new Vector3f(0.5f, 0.5f, 0.0f).mult(50));
    arrow.setMaterial(matBullet);

  5. We attach it to rootNode as well as to physicsSpace, as shown in the following code snippet:

    rootNode.attachChild(arrow); getPhysicsSpace().add(arrow);

How it works...

The Arrow object has two major components. One is Geometry, which is a simple elongated box. The other is CollisionShape for the head of the arrow, which is the only thing that will look for collisions. The geometry is conveniently moved so that its tip will be at the (0,0,0) position of the Arrow node. It is convenient since it means we don't have to do any conversions in ArrowFacingControl but can use rotateUpTo with the actual velocity (direction) of the arrow.

Handling multiple gravity sources

Some games require handling gravity from multiple variable sources. In this recipe, we'll handle this and create a simple miniature solar system to demonstrate it using ThrusterControl from the Building a rocket engine recipe. To (greatly) simplify the relation between the planets, they won't affect each other with their gravity, but only the ship. It will also be made in a 2D-asteroids-like fashion, although the gravity would still apply for a 3D game.

We'll add some basic controls to rotate the ship to the left and right, and you can use the thruster to make the ship move forward.

How to do it...

Apart from ThrusterControl, we'll create two more small classes and an application class that joins everything together. Let's start with a class that represents the player's ship. This will consist of the following six steps:

  1. Create a new class called SpaceShip, which has a Node field called shipNode in it.
  2. In the constructor, we set up the physics for it by creating a new RigidBodyControl instance with BoxCollisionShape. To create it in a way that it is affected by gravity, we also give it a mass of 1 that will be supplied in the constructor as follows:

    RigidBodyControl control = new RigidBodyControl(new BoxCollisionShape(new Vector3f(1, 1, 1)), 1);
    shipNode.addControl(control);

  3. Now, we create a Node instance called thruster. We also set the name of Node to Thruster for the control to find it automatically, as shown in the following line of code:

    Node thruster = new Node("Thruster");

  4. We set localTranslation to be at one of the sides of the spaceship and attach it to shipNode, as follows:

    thruster.setLocalTranslation(-1, 0, 0); shipNode.attachChild(thruster);

  5. Then, we rotate the ship's spatial so that it's facing sideways:

    shipNode.rotate(0, FastMath.PI, 0);

  6. Finally, we add a new ThrusterControl instance to the spaceship's spatial.

That's it for the SpaceShip class. Now, we create a class for our planets, as follows:

  1. We start off by defining a class called StellarBody, which extends AbstractControl. The StellarBody class has four float fields: size, speed, orbit, and cycle.
  2. The constructor takes three of these (size, speed, and orbit) as the input, as shown in the following code:

    public StellarBody(float orbit, float speed, float size)

  3. We override the setSpatial method and add RigidBodyControl to the supplied spatial with SphereCollisionShape, using size as the radius and 0 for mass:

    RigidBodyControl rigidBody = new RigidBodyControl(new SphereCollisionShape(size), 0f);
    rigidBody.setGravity(Vector3f.ZERO); spatial.addControl(rigidBody);

  4. In the controlUpdate method, we make it move along its orbit by increasing the speed of the cycle by multiplying it by tpf, as follows:

    cycle += (speed * tpf) % FastMath.TWO_PI;

  5. Then, we set the actual position of the planet along the orbit using the sin and cos methods of the FastMath class:

    float x = FastMath.sin(cycle); float z = FastMath.cos(cycle);

  6. We multiply the result by the orbit and set localTranslation of the spatial to the new location as follows:

    spatial.setLocalTranslation(x * orbit, 0, z * orbit);

  7. Then, we also need to set physicsLocation of RigidBodyControl to the same location.
  8. We need a new method, getGravity, that will take the position of the ship as an input Vector3f.
  9. The method begins by subtracting the input position by worldTranslation, to get the position of the ship relative to the StellarBody class, as follows:

    Vector3f relativePosition = spatial.getWorldTranslation(). subtract(position);

  10. The result is normalized and then modified by a formula to get a suitable gravity. This value is returned to the calling method, as follows:

    relativePosition.normalizeLocal(); return relativePosition.multLocal(size * 1000 / relativePosition.lengthSquared());

To test all of this, we need to add a few things to SimpleApplication. To do this, perform the following set of steps:

  1. First of all, we implement AnalogListener.
  2. We add an ArrayList<StellarBody> list called gravitationalBodies.
  3. In the simpleInitApp method, we should begin by initializing bulletAppState and set up some controls for the spaceship. We add actions to rotate the spaceship to the left and right as well as fire the ship's thruster, as follows:

    String[] mappings = new String[]{"rotateLeft", "rotateRight", "boost"}; inputManager.addListener(this, mappings);
    inputManager.addMapping("boost", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addMapping("rotateLeft", new KeyTrigger(KeyInput.KEY_LEFT));
    inputManager.addMapping("rotateRight", new KeyTrigger(KeyInput.KEY_RIGHT));

  4. Since it's a 2D representation, we move the camera some distance up and make it look as if it is at the center of the world. This can be implemented as follows:

    cam.setLocation(new Vector3f(0, 300f, 0)); cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);

  5. We create an instance called ship of SpaceShip and attach its geometry to rootNode and physicsSpace of bulletAppState.
  6. Now we can create a number of StellarBody instances using the following steps:
    1. For each instance, we should create a Geometry class with a Sphere shape that will have the same radius as the size we will supply to the StellarBody control.
    2. The Geometry class should both be attached to rootNode and physicsSpace of bulletAppState.
    3. We add StellarBody as a control to the Geometry class and the gravitationalBodies list.
  7. Inside the update method, we have to take into account the gravity of the StellarBody instances.
  8. First, we define a new Vector3f instance called combinedGravity.
  9. Then, we loop through our gravitationalBodies list and apply the following line of code to apply the gravity to combinedGravity:

    combinedGravity.addLocal(body.getGravity(ship.getSpatial(). getWorldTranslation()));

  10. Finally, we call the ship.setGravity(combinedGravity); statement.

How it works...

Due to the extreme difficulty in creating a stable solar system with more than three bodies, StellarBody controls the need to have a static orbit around the center of the system. Using 0 as mass ensures that they aren't affected by gravity. The orbit field represents the orbit's distance from the center of the system, and it will rotate around the center using speed as a factor. The cycle field stores information on how far along its orbit it has come, and will reset once it reaches two PI (a full circle).

The getGravity method returns the gravity relative to the position that is supplied, which in this case is the location of the ship. It first determines the direction and then applies the gravity based on the distance between the two.

By using the gravitationalBodies list, we have a dynamic way to simply add up all the gravitational forces in the system to a single Vector3f object, which we then apply to the spaceship in the update method of the application.

Self-balancing using RotationalLimitMotors

Many games today use a blend of animations and physics to create realistic movement.

For animated characters, this revolves around balance. It could take the shape of a runner who leans inwards through a curve to counter the centrifugal force. Creating a system like this is not easy and requires a lot of tweaking. In this recipe, we'll look into some of the fundamentals of this, and we'll create a new Control class that will try to balance itself using the rotational motors of SixDofJoint.

Six Degrees of Freedom (SixDof) relates to the six ways the joint can rotate:

+x, -x, +y, -y, +z, and -z. One way it differs from a point2point joint is that in addition, it also has motors for each axis, which makes it possible for it to also apply force.

How to do it...

To simulate balancing, we will begin by creating the upper body of a stickman-shape figure with a torso and two rigid arms. To do this, perform the following set of steps:

  1. First of all, we should set up an application with BulletAppState.
  2. In the simpleInitApp method, we create a small square Box Geometry to be the waist of the character. It can be 0.25f in all the axes.
  3. We add RigidBodyControl to it with 0 in mass since it shouldn't move.
  4. Then, we create an oblong box to be the torso and place it above the waist. It should have RigidBodyControl with 1 in mass and BoxCollisionShape should be of the same size as the geometry:

    torso = new Geometry("Torso", new Box(0.25f, 2f, 0.25f); RigidBodyControl torsoRigidBody = new RigidBodyControl
    (new BoxCollisionShape(...), 1f); ... torsoRigidBody.setPhysicsLocation(new Vector3f(0, 4.25f, 0));

Summary

In this article we learned to use physics in games and created a pushable door, build a rocket engine, projectiles and arrow, handle multiple gravity sources, and self balance using RotationalLimitMotors.

Resources for Article:


Further resources on this subject:


Books to Consider

comments powered by Disqus