Learning Bullet with LibGDX

In the next sections, you will learn how to use Bullet libraries in LibGDX.

Setting up a project

If you are using the old LibGDX project generation (gdx-setup-ui.jar) method, then you'll need to add gdx-bullet.jar to your main project. Alternatively, you can add the gdx-bullet project to the projects of the build path of your main project. For your desktop project, you'll need to add the gdx-bullet-natives.jar file to the libraries. For your android project, you'll need to copy the armeabi/libgdx-bullet.so file and armeabi-v7a/libgdx-bullet.so file to the libs folder in your android project.

Bullet isn't supported for GWT at the moment. Alternatively, we can use the LibGDX Gradle Project Setup (gdx-setup.jar) tool where Bullet will be linked altogether and you don't have to worry about it.

Open the gdx-setup.jar file and enter the following details:

  • Name: CollisionTest
  • Package: com.packtpub.libgdx.collisiontest
  • Game class: MyCollisionTest
  • Destination: C:libgdx
  • Android SDK: <Path to your android-sdk>

Select the latest LibGDX version and check Android, Desktop, iOS as Sub Projects. We will avoid Html as Bullet does not support GWT at the moment. Now, select Bullet under the Extensions menu and click on Generate, as shown in the following screenshot. Now, you can follow the steps in Chapter 1, Introduction to LibGDX and Project Setup, under the Creating a new application section, to generate and import the LibGDX project.

Setting up a project

Creating a basic 3D scene

In Chapter 13, Basic 3D Programming, you learned how to create a basic model. Let's do it again. Create a simple scene with a ball and ground, as shown in the following screenshot:

Creating a basic 3D scene

Add the following code to MyCollisionTest.java:

package com.packtpub.libgdx.collisiontest;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
...
import com.badlogic.gdx.utils.Array;

public class MyCollisionTest extends ApplicationAdapter {
PerspectiveCamera cam;
ModelBatch modelBatch;
Array<Model> models;
ModelInstance groundInstance;
ModelInstance sphereInstance;
Environment environment;
ModelBuilder modelbuilder;

@Override
public void create() {
   modelBatch = new ModelBatch();

   environment = new Environment();
   environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
   environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));

   cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
   cam.position.set(0, 10, -20);
   cam.lookAt(0, 0, 0);
   cam.update();

   models = new Array<Model>();

   modelbuilder = new ModelBuilder();
   // creating a ground model using box shape
   float groundWidth = 40;
   modelbuilder.begin();
   MeshPartBuilder mpb = modelbuilder.part("parts", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal | Usage.Color, new Material(ColorAttribute.createDiffuse(Color.WHITE)));
   mpb.setColor(1f, 1f, 1f, 1f);
   mpb.box(0, 0, 0, groundWidth, 1, groundWidth);
   Model model = modelbuilder.end();
   models.add(model);
   groundInstance = new ModelInstance(model);

   // creating a sphere model
   float radius = 2f;
   final Model sphereModel = modelbuilder.createSphere(radius, radius, radius, 20, 20, new Material(ColorAttribute.createDiffuse(Color.RED), ColorAttribute.createSpecular(Color.GRAY), FloatAttribute.createShininess(64f)), Usage.Position | Usage.Normal);
   models.add(sphereModel);
   sphereInstance = new ModelInstance(sphereModel);
   sphereinstance.transform.trn(0, 10, 0);
}

public void render() {
   Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
   Gdx.gl.glClearColor(0, 0, 0, 1);
   Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

   modelBatch.begin(cam);
   modelBatch.render(groundInstance, environment);
   modelBatch.render(sphereInstance, environment);
   modelBatch.end();
}

@Override
public void dispose() {
   modelBatch.dispose();
   for (Model model : models)
          model.dispose();

}
}

The ground is actually a thin box created using ModelBuilder just like the sphere. Now that we have created a simple 3D scene, let's add some physics using the following code:

public class MyCollisionTest extends ApplicationAdapter {
...

private btDefaultCollisionConfiguration collisionConfiguration;
private btCollisionDispatcher dispatcher;
private btDbvtBroadphase broadphase;
private btSequentialImpulseConstraintSolver solver;
private btDiscreteDynamicsWorld world;

private Array<btCollisionShape> shapes = new Array<btCollisionShape>();
private Array<btRigidBodyConstructionInfo> bodyInfos = new Array<btRigidBody.btRigidBodyConstructionInfo>();
private Array<btRigidBody> bodies = new Array<btRigidBody>();
private btDefaultMotionState sphereMotionState;

@Override
public void create() {
...
    // Initiating Bullet Physics 
   Bullet.init();


    //setting up the world
   collisionConfiguration = new btDefaultCollisionConfiguration();
   dispatcher = new btCollisionDispatcher(collisionConfiguration);
   broadphase = new btDbvtBroadphase();
   solver = new btSequentialImpulseConstraintSolver();
   world = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
   world.setGravity(new Vector3(0, -9.81f, 1f));


   // creating ground body
   btCollisionShape groundshape = new btBoxShape(new Vector3(20, 1 / 2f, 20));
      shapes.add(groundshape);
   btRigidBodyConstructionInfo bodyInfo = new btRigidBodyConstructionInfo(0, null, groundshape, Vector3.Zero);
   this.bodyInfos.add(bodyInfo);
   btRigidBody body = new btRigidBody(bodyInfo);
   bodies.add(body);

   world.addRigidBody(body);

   // creating sphere body
  sphereMotionState = new btDefaultMotionState(sphereInstance.transform);
   sphereMotionState.setWorldTransform(sphereInstance.transform);
   final btCollisionShape sphereShape = new btSphereShape(1f);
   shapes.add(sphereShape);

   bodyInfo = new btRigidBodyConstructionInfo(1, sphereMotionState, sphereShape, new Vector3(1, 1, 1));
   this.bodyInfos.add(bodyInfo);

   body = new btRigidBody(bodyInfo);
   bodies.add(body);
   world.addRigidBody(body);
}

public void render() {
   Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
   Gdx.gl.glClearColor(0, 0, 0, 1);
   Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

   world.stepSimulation(Gdx.graphics.getDeltaTime(), 5);
   sphereMotionState.getWorldTransform(sphereInstance.transform);

   modelBatch.begin(cam);
   modelBatch.render(groundInstance, environment);
   modelBatch.render(sphereInstance, environment);
   modelBatch.end();
}

@Override
public void dispose() {
   modelBatch.dispose();
   for (Model model : models)
          model.dispose();
   for (btRigidBody body : bodies) {
          body.dispose();
   }
   sphereMotionState.dispose();
   for (btCollisionShape shape : shapes)
          shape.dispose();
   for (btRigidBodyConstructionInfo info : bodyInfos)
          info.dispose();
   world.dispose();
   collisionConfiguration.dispose();
   dispatcher.dispose();
   broadphase.dispose();
   solver.dispose();
   Gdx.app.log(this.getClass().getName(), "Disposed");
}
}

The highlighted parts are the addition to our previous code. After execution, we see the ball falling and colliding with the ground.

Initializing Bullet

We know that LibGDX uses a wrapper to call the C++ Bullet library. So, before calling any of the Bullet functions, we have to load the Bullet library to memory. To do this, we call Bullet.init() in the create() method. Calling any of the Bullet functions, for example, btDefaultCollisionConfiguration() before Bullet.init() will result in an error.

Creating a dynamics world

After initializing Bullet, we create the virtual physics world where everything happens. To do this, we add the following code:

   collisionConfiguration = new btDefaultCollisionConfiguration();
   dispatcher = new btCollisionDispatcher(collisionConfiguration);
   broadphase = new btDbvtBroadphase();
   solver = new btSequentialImpulseConstraintSolver();
   world = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
   world.setGravity(new Vector3(0, -9.81f, 1f));

Collision detection in a 3D world is complex. We can use specialized collision detection algorithms, however, they are very expensive if we use them to check all bodies at a time. Ideally, we'd first check whether the two objects are near each other, for example, using a bounding box or bounding sphere, and only if they are near each other, we'd use the more accurate and specialized collision algorithm. This two phase method has benefits. The first phase, where we find collision objects that are near each other, is called the broad phase. Then, the second phase, where a more accurate specialized collision algorithm is used, is called the near phase. In practice, the collision dispatcher is the class we've used for the near phase.

To construct the dynamics world, we'll need a constraint solver and a collision configuration. The constraint solver is used to attach objects to each other. Also, btCollisionConfiguration allows the Bullet collision detection stack allocator and pool memory allocators to be configured. This collision configuration is also fed to the collision dispatcher through its constructor and then we create our dynamic world by calling btDiscreteDynamicsWorld. The btDiscreteDynamicsWorld class is a subclass of btDynamicsWorld, which is a subclass of btCollisionWorld. When the world is created, we define its gravity using the setGravity() function.

A custom MotionState class

In a 3D world with many physics objects, all might not be at motion at the same time. For each frame render, if we iterate and update positions of all render objects we're simulating, it would require a lot of time especially if the game has a lot of physics bodies. Luckily, the Bullet wrapper offers callback methods that will be called when a certain event occurs. We create a custom interface extending the btMotionState class where we include what to do when something happens. For example, create a new MyMotionState.java file in the com.packtpub.libgdx.collisiontest package and add the following code:

public class MyMotionState extends btMotionState {
final ModelInstance instance;
public MyMotionState (ModelInstance instance) {
   this.instance = instance;
}
@Override
public void getWorldTransform(Matrix4 worldTrans) {
   worldTrans.set(instance.transform);
}
@Override
public void setWorldTransform(Matrix4 worldTrans) {
   instance.transform.set(worldTrans);
}

}

The setWorldTransform() function will set the transformation of the render object, whereas the getWorldTransform() function returns the transformation of the current render object. This custom class will update the render instance when Bullet updates the position of respective physics object.

A simple ContactListener class

LibGDX Bullet wrapper offers callback methods to notify us when a collision occurs. Here, we can define what should happen when a collision occurs. This is similar to contact listener in Box2D. However, this callback class, ContactListener, is not a Bullet class but a class specifically created for the Bullet wrapper. For this reason, we do not have to inform Bullet to use ContactListener.

Create a new MyContactListener.java file in the com.packtpub.libgdx.collisiontest package and add the following class:

public class MyContactListener extends ContactListener {
@Override
public void onContactStarted(btCollisionObject colObj0, btCollisionObject colObj1) {
   Gdx.app.log(this.getClass().getName(), "onContactStarted");

}
}

In the create() method of our game class, we simply call the following method:

   MyContactListener contactListener = new MyContactListener();

Note

Bullet contact listener provides a lot of methods for various collision states. For more information, visit https://github.com/libgdx/libgdx/wiki/Bullet-physics#contact-listeners.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.239.44