Bullet Physics Tutorial: Getting Started

Learn how to make a simple OpenGL ES Arkanoid game have 3D physics and collision detection in this Bullet Physics tutorial! By Kirill Muzykov.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Rigid Bodies

I’ll use the definition of a rigid body from the Bullet wiki:

A “rigid body” is what it sounds like – an item of fixed mass, size, and other physical properties. It’s the base individual item in physics simulations.

In addition to rigid bodies, Bullet supports soft bodies:

Imagine a rigid body, but squidgy. The mass doesn’t change, but the shape of it can.

In this tutorial, you’re going to use only rigid bodies. After all, in Breakout the boxes and ball aren’t squishy! From here forward, when I say body or physics body, I mean a rigid body.

Okay, back to your game. Now that there is a world, you need to fill it with objects. Every simulated object must have a body. You’ll have several physics-enabled objects with rigid bodies:

  • Ball
  • Paddle
  • Border
  • Brick

All of these objects will share common code, so it would be nice if they could have a common parent class, but you also want to keep their existing properties.

The best way to do this is to create a special class called PNode (for “physics node”) and inherit it from RWNode. All physics objects will inherit from PNode and will have the best of both worlds! Just as RWNode represented a visual object for your earlier custom code describing object interactions in the game, the PNode superclass will represent the basic idea of a physical object, to be manipulated by the Bullet physics simulation engine.

Creating the PNode Class

Switch to Xcode if you don’t have it open already and create a new Objective-C class called PNode. Make it a subclass of RWNode and put it in the Nodes group alongside RWNode.

pnode_creation

Right after that, rename PNode.m to PNode.mm. Yes, you’re going to write some C++ code again.

Open PNode.h and include this header at the top, right after #import “RWNode.h”:

#import "RWNode.h"
#include "btBulletDynamicsCommon.h"

Add the following init method right after the @interface line:

- (instancetype)initWithName:(char *)name
                        mass:(float)mass   //1
                      convex:(BOOL)convex  //2
                         tag:(int)tag      //3
                      shader:(GLKBaseEffect *)shader
                    vertices:(Vertex *)vertices
                 vertexCount:(unsigned int)vertexCount
                 textureName:(NSString *)textureName
               specularColor:(GLKVector4)specularColor
                diffuseColor:(GLKVector4)diffuseColor
                   shininess:(float)shininess;

I’ve marked new parameters with numbered comments. Here is some brief information about each of them:

For the sake of this tutorial, though, you’ll implement the creation of both convex and concave objects. Since creating convex objects in Bullet differs from creating concave objects, you need a flag.

  • The mass of a static object equals zero. These are immovable objects. In your game, the border is a static object.
  • The mass of a kinematic object also equals zero, but you can move such objects with code by explicitly setting their position and rotation. In your game, the paddle and bricks are kinematic objects.
  • The mass of a dynamic object is non-zero. You move such objects by applying a force to them. In your game, the ball is a dynamic object. You’ll set its direction and velocity and let the physics engine do the work. When the ball hits a border or a brick, it will bounce back, but it can never affect the positions of the border, brick or paddle since they are immovable.
  1. mass is pretty obviously the mass of the object, but it has another not-so-obvious significance. In Bullet, you can have three types of objects, depending on their mass:
  2. convex is a flag describing whether the current physics object is convex or concave. You should always try to use convex objects because physics engines work much faster with them. You can always decompose a concave object into several convex objects.
  3. You are using tag in collision detection to determine what types of objects collided.

Now add two properties right below init in PNode.h:

//1
@property (nonatomic, readonly) btRigidBody* body;
//2
@property (nonatomic, assign)   int tag;

Here are descriptions of these properties:

  1. body is the reference to a rigid body that you’ll create and store in PNode. Using this property, you’ll allow the game scene to work with the physics body of the node.
  2. tag gets you access to the tag value you used to create the current PNode.

It’s time to switch to PNode.mm. Add a new private instance variable:

@implementation PNode {
    btCollisionShape* _shape;
}

_shape describes the shape of the physics body. btCollisionShape is an abstract class, and there are several different implementations of collision shapes. For example, you can describe the shape of an object as a sphere using btSphereShape or you can create complicated shapes with btBvhTriangleMeshShape, specifying vertices of triangles just like you do when rendering complex objects in OpenGL.

Of course, physics engines work faster with simple shapes, so you should try to use as many spheres and planes as possible. In this tutorial, though, you’ll reuse the vertices of the models exported from Blender and create complicated physics shapes with only few lines of code.

You can read more about collision shapes here.

Creating a Body

Body creation consists of two steps:

  1. Create the shape using one of the Bullet classes.
  2. Create the body construction info and use it to actually create the body.

Start with the shape creation method by placing the following in PNode.mm:

-(void)createShapeWithVertices:(Vertex *)vertices count:(unsigned int)vertexCount isConvex:(BOOL)convex
{
    //1
    if (convex)
    {
        //2
        _shape = new btConvexHullShape();
        for (int i = 0; i < vertexCount; i++)
        {
            Vertex v = vertices[i];
            btVector3 btv = btVector3(v.Position[0], v.Position[1], v.Position[2]);
            ((btConvexHullShape*)_shape)->addPoint(btv);
        }
    }
    else
    {
        //3
        btTriangleMesh* mesh = new btTriangleMesh();
        for (int i=0; i < vertexCount; i += 3)
        {
            Vertex v1 = vertices[i];
            Vertex v2 = vertices[i+1];
            Vertex v3 = vertices[i+2];
            
            btVector3 bv1 = btVector3(v1.Position[0], v1.Position[1], v1.Position[2]);
            btVector3 bv2 = btVector3(v2.Position[0], v2.Position[1], v2.Position[2]);
            btVector3 bv3 = btVector3(v3.Position[0], v3.Position[1], v3.Position[2]);
            
            mesh->addTriangle(bv1, bv2, bv3);
        }        
		   _shape = new btBvhTriangleMeshShape(mesh, true);
    }
}

Here’s an explanation of createShapeWithVertices::

  1. It takes different approaches to create convex and concave shapes, which is why you need that flag in init as well as this check.
  2. In case of a convex object, you use btConvexHullShape. This class allows you to add all points of the object and uses them to automatically create the minimum convex hull for it.
  3. In case of a concave object, you use a more complicated class called btBvhTriangleMeshShape. This class requires the creation of a mesh object consisting of triangles. In this step, you gather triangles by grouping vertices from the list of vertices. Then you create a mesh and create a shape object from this mesh.

Next you need to write a method creating the body of the object. Place the following method in PNode.mm, right below createShapeWithVertices:

-(void)createBodyWithMass:(float)mass
{
    //1
    btQuaternion rotation;
    rotation.setEulerZYX(self.rotationZ, self.rotationY, self.rotationX);
    
    //2
    btVector3 position = btVector3(self.position.x, self.position.y, self.position.z);
    
    //3
    btDefaultMotionState* motionState = new btDefaultMotionState(btTransform(rotation, position));
    
    //4
    btScalar bodyMass = mass;
    btVector3 bodyInertia;
    _shape->calculateLocalInertia(bodyMass, bodyInertia);
    
    //5
    btRigidBody::btRigidBodyConstructionInfo bodyCI = btRigidBody::btRigidBodyConstructionInfo(bodyMass, motionState, _shape, bodyInertia);
    
    //6
    bodyCI.m_restitution = 1.0f;
    bodyCI.m_friction = 0.5f;
    
    //7
    _body = new btRigidBody(bodyCI);

	//8
    _body->setUserPointer((__bridge void*)self);
    
    //9
    _body->setLinearFactor(btVector3(1,1,0));
}

This code definitely won’t suffer from a little description:

I strongly advise you to understand quaternions, since most 3D games use them for rotations. There is a good tutorial from Ray that covers rotations and quaternions here.

For the sake of simplicity, you won’t use motion state in this game, so here you just create and use the default implementation. You can read more about motion states
here.

Setting the property bodyCI.m_friction to non-zero will make the ball spin, just like it would in real life if you launch the ball into a wall at some angle other than 90 degrees.

This is important moment. Sometimes you only have access to a physics body – for example, when Bullet calls your callback and passes you the body – but you want to get the node object that holds this body. In this line, you’re making that possible.

  • With a value of 0.0, it doesn’t bounce at all. The sphere will stick to the floor on the first touch.
  • With a value between 0 and 1, the object bounces, but with each bounce loses part of its energy. The sphere will bounce several times, each time lower than the previous bounce until finally it stops.
  • With a value of more than 1, the object gains energy with each bounce. This is not very realistic, or at least I can’t think of a real-life object that behaves this way. Your sphere will bounce higher than the point from which it was dropped, then it will bounce even higher and so on, until it bounces right into space.
  1. You specify the object’s rotation. Bullet uses quaternions to represent object rotation. Quaternions are a complicated topic and this tutorial will not cover them. For now, just remember that a quaternion holds information about the object’s rotation.
  2. You specify the object’s position. At this point, both position and rotation are set to zero in super init, but it’s better to use property values in case you change position and rotation initialization in RWNode’s init.
  3. MotionState is a convenient class that allows you to sync a physical body and with your drawable objects. You don’t have to use motion states to set/get the position and rotation of the object, but doing so will get you several benefits, including interpolation and callbacks.
  4. You set the mass and inertia values for the shape. You don’t have to calculate inertia for your shape manually. Instead you are using a utility function that takes a reference, btVector3, and sets the correct inertia value using the shape’s data.
  5. To create a body, you have to fill out ConstructionInfo. This structure contains all the required properties to construct the body. Think of it as a convenience. You pass one structure to the constructor instead of having dozens of parameters, most of which you will set to default values.
  6. ConstructionInfo takes only a couple of important parameters and sets all other properties to default values. You need to change a few properties. bodyCI.m_restitution sets an object’s bounciness. Imagine dropping a ball – a sphere – to the floor:
  7. You’ve gathered everything to create a body, and here you’re doing just that. There’s not much of interest here, since you’re just passing ConstructionInfo holding all the parameters you set earlier.
  8. Here you save a reference to the PNode class in the body object. When you process collisions, I’ll show you how to get it back.
  9. Here’s another interesting moment. You’re limiting object movement to a 2D plane (x,y). Breakout is actually a 2D game with 3D models, and all objects have to be in one plane to make collisions work. This keeps your ball and other objects from bouncing somewhere along the z-axis.
Kirill Muzykov

Contributors

Kirill Muzykov

Author

Over 300 content creators. Join our team.