| name | libgdx-bullet-physics |
| description | Use when writing libGDX Java/Kotlin code involving 3D physics with the Bullet wrapper — btRigidBody, btDiscreteDynamicsWorld, collision shapes, ContactListener, raycasting, MotionState, or debug drawing. Use when debugging segfaults, native memory leaks, bodies not moving, or collision detection failures in the libGDX Bullet extension. |
libGDX Bullet Physics Wrapper
Quick reference for the libGDX Bullet extension (com.badlogic.gdx.physics.bullet.*). This is a JNI wrapper around the C++ Bullet Physics library. All bt* objects are backed by native memory — the Java GC does NOT free them. Manual dispose() is mandatory.
Not supported on HTML5/GWT.
Gradle Dependencies
// core
api "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
// desktop
implementation "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-desktop"
// android (all four)
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-armeabi-v7a"
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-arm64-v8a"
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-x86"
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-x86_64"
// ios
implementation "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-ios"
Initialization
Bullet.init();
Three overloads:
Bullet.init()
Bullet.init(boolean useRefCounting)
Bullet.init(boolean useRefCounting, boolean logging)
Call once in create(), guard against double-init. Reference counting enables automatic obtain()/release() on shared objects (e.g., btCompoundShape child shapes).
Dynamics World Setup
Five objects, created in this order:
btDefaultCollisionConfiguration collisionConfig = new btDefaultCollisionConfiguration();
btCollisionDispatcher dispatcher = new btCollisionDispatcher(collisionConfig);
btDbvtBroadphase broadphase = new btDbvtBroadphase();
btSequentialImpulseConstraintSolver solver = new btSequentialImpulseConstraintSolver();
btDiscreteDynamicsWorld world = new btDiscreteDynamicsWorld(
dispatcher, broadphase, solver, collisionConfig);
world.setGravity(new Vector3(0, -10f, 0));
Constructor: btDiscreteDynamicsWorld(btDispatcher, btBroadphaseInterface, btConstraintSolver, btCollisionConfiguration). Parameter order is dispatcher, broadphase, solver, config.
Stepping
world.stepSimulation(delta, maxSubSteps, fixedTimeStep);
float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());
world.stepSimulation(delta, 5, 1f / 60f);
DO NOT use the 1-arg form stepSimulation(delta) — it defaults maxSubSteps=1, so any frame taking longer than 1/60s loses simulation time. Always pass all three arguments.
Adding / Removing Bodies
world.addRigidBody(body);
world.addRigidBody(body, collisionGroup, collisionMask);
world.removeRigidBody(body);
There is NO removeAndDestroyBody() — you must remove then dispose separately.
Collision Shapes
All in com.badlogic.gdx.physics.bullet.collision. Constructors take libGDX Vector3 (not btVector3).
| Shape | Constructor | Notes |
|---|
btBoxShape | (Vector3 halfExtents) | Half-extents, not full size |
btSphereShape | (float radius) | |
btCapsuleShape | (float radius, float height) | Y-axis. Also btCapsuleShapeX, btCapsuleShapeZ |
btCylinderShape | (Vector3 halfExtents) | Y-axis. Also btCylinderShapeX, btCylinderShapeZ |
btConeShape | (float radius, float height) | Y-axis. Also btConeShapeX, btConeShapeZ |
btConvexHullShape | () then addPoint(Vector3) | Or (FloatBuffer) — must be direct buffer |
btBvhTriangleMeshShape | (Array<MeshPart>) | Static concave meshes only. libGDX convenience |
btCompoundShape | () | addChildShape(Matrix4 localTransform, btCollisionShape) |
Gotchas:
btBvhTriangleMeshShape is for static geometry only — do not use for dynamic bodies.
btConvexHullShape(FloatBuffer) requires a direct buffer (ByteBuffer.allocateDirect()).
- Capsule/Cylinder/Cone default to Y-axis. Use the
X/Z suffixed classes for other axes.
btCompoundShape.addChildShape() calls obtain() on child shapes when reference counting is enabled.
calculateLocalInertia
Vector3 localInertia = new Vector3();
shape.calculateLocalInertia(mass, localInertia);
On btCollisionShape. Must call for dynamic bodies (mass > 0). Static bodies use Vector3.Zero.
Rigid Bodies
Static vs Dynamic vs Kinematic
| Type | Mass | Inertia | Flags | Moved by |
|---|
| Static | 0 | Vector3.Zero | (auto CF_STATIC_OBJECT) | Nothing |
| Dynamic | > 0 | calculateLocalInertia() | (none needed) | Physics |
| Kinematic | 0 | Vector3.Zero | CF_KINEMATIC_OBJECT | Application code via MotionState |
Construction
Two patterns — both work. Direct construction bypasses the info object:
Vector3 localInertia = new Vector3();
shape.calculateLocalInertia(mass, localInertia);
btRigidBody.btRigidBodyConstructionInfo info =
new btRigidBody.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
info.setRestitution(0.5f);
info.setFriction(0.8f);
btRigidBody body = new btRigidBody(info);
info.dispose();
btRigidBody body = new btRigidBody(mass, motionState, shape, localInertia);
btRigidBodyConstructionInfo is an inner class of btRigidBody. Constructor: (float mass, btMotionState motionState, btCollisionShape shape, Vector3 localInertia). The motionState parameter can be null — set it later via body.setMotionState().
Kinematic Body Setup
btRigidBody body = new btRigidBody(0f, motionState, shape, Vector3.Zero);
body.setCollisionFlags(body.getCollisionFlags()
| btCollisionObject.CollisionFlags.CF_KINEMATIC_OBJECT);
body.setActivationState(Collision.DISABLE_DEACTIVATION);
Without DISABLE_DEACTIVATION, Bullet stops calling getWorldTransform() on the MotionState after the body sleeps — your kinematic body freezes.
Key btRigidBody Methods
body.applyCentralForce(Vector3 force);
body.applyForce(Vector3 force, Vector3 relPos);
body.applyTorque(Vector3 torque);
body.clearForces();
body.applyCentralImpulse(Vector3 impulse);
body.applyImpulse(Vector3 impulse, Vector3 relPos);
body.setLinearVelocity(Vector3 vel);
body.setAngularVelocity(Vector3 vel);
body.setLinearFactor(new Vector3(1, 1, 0));
body.setAngularFactor(new Vector3(0, 0, 1));
body.setDamping(float linearDamping, float angularDamping);
body.setSleepingThresholds(float linear, float angular);
body.activate();
btCollisionObject (base class)
body.setWorldTransform(Matrix4 transform);
body.getWorldTransform(Matrix4 out);
Matrix4 t = body.getWorldTransform();
body.setUserValue(int value);
int v = body.getUserValue();
body.userData = myObject;
body.setCollisionFlags(int flags);
body.setActivationState(int state);
body.isStaticOrKinematicObject();
Collision Flags (btCollisionObject.CollisionFlags)
| Constant | Value | Use |
|---|
CF_STATIC_OBJECT | 1 | Auto-set for mass=0 bodies |
CF_KINEMATIC_OBJECT | 2 | Must set manually for kinematic |
CF_NO_CONTACT_RESPONSE | 4 | Trigger/sensor (detects but no physics response) |
CF_CUSTOM_MATERIAL_CALLBACK | 8 | Required for onContactAdded |
CF_CHARACTER_OBJECT | 16 | For character controllers |
Activation States (com.badlogic.gdx.physics.bullet.collision.Collision)
| Constant | Value |
|---|
Collision.ACTIVE_TAG | 1 |
Collision.ISLAND_SLEEPING | 2 |
Collision.WANTS_DEACTIVATION | 3 |
Collision.DISABLE_DEACTIVATION | 4 |
Collision.DISABLE_SIMULATION | 5 |
MotionState
Syncs physics transform with render transform. Bullet calls setWorldTransform after simulating dynamic bodies, and getWorldTransform for kinematic bodies.
static class MyMotionState extends btMotionState {
private final Matrix4 transform;
public MyMotionState(Matrix4 transform) {
this.transform = transform;
}
@Override
public void getWorldTransform(Matrix4 worldTrans) {
worldTrans.set(transform);
}
@Override
public void setWorldTransform(Matrix4 worldTrans) {
transform.set(worldTrans);
}
}
MyMotionState motionState = new MyMotionState(modelInstance.transform);
btRigidBody body = new btRigidBody(mass, motionState, shape, localInertia);
btDefaultMotionState also exists (com.badlogic.gdx.physics.bullet.linearmath) but the custom pattern above is standard in libGDX because it shares the Matrix4 directly with ModelInstance.
Gotcha: MotionState takes com.badlogic.gdx.math.Matrix4 — the wrapper converts to/from native Bullet transforms automatically.
Contact Detection
ContactListener
com.badlogic.gdx.physics.bullet.collision.ContactListener — instantiate to enable globally. Only one overload per callback event.
Four event types, each with multiple overloads (btCollisionObject, userValue int, or btCollisionObjectWrapper variants). The int userValue variants are fastest (no JNI object mapping).
ContactListener contactListener = new ContactListener() {
@Override
public boolean onContactAdded(int userValue0, int partId0, int index0,
boolean match0, int userValue1, int partId1, int index1, boolean match1) {
if (match0) { }
return true;
}
};
Filtering setup:
body.setContactCallbackFlag(1 << 1);
body.setContactCallbackFilter(1 << 1);
Match logic: callback fires if (objA.callbackFilter & objB.callbackFlag) != 0 for at least one side.
onContactAdded requires CF_CUSTOM_MATERIAL_CALLBACK on at least one colliding body:
body.setCollisionFlags(body.getCollisionFlags()
| btCollisionObject.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);
| Callback | Returns | Fires when |
|---|
onContactAdded | boolean | New contact point created. Needs CF_CUSTOM_MATERIAL_CALLBACK. |
onContactProcessed | void | Contact processed during simulation step |
onContactStarted | void | New manifold between two objects |
onContactEnded | void | Manifold between two objects removed |
Enable/disable individually: enableOnAdded() / disableOnAdded(), etc.
Raycasting
ClosestRayResultCallback callback = new ClosestRayResultCallback(Vector3.Zero, Vector3.Z);
callback.setCollisionObject(null);
callback.setClosestHitFraction(1f);
callback.setRayFromWorld(rayFrom);
callback.setRayToWorld(rayTo);
world.rayTest(rayFrom, rayTo, callback);
if (callback.hasHit()) {
btCollisionObject hit = callback.getCollisionObject();
Vector3 hitPoint = new Vector3();
callback.getHitPointWorld(hitPoint);
Vector3 hitNormal = new Vector3();
callback.getHitNormalWorld(hitNormal);
}
callback.dispose();
There is NO set(from, to) convenience method — you must call individual setters to reuse.
AllHitsRayResultCallback — same constructor pattern. Results via getCollisionObjects() (returns btCollisionObjectConstArray, iterate with atConst(int index)), getHitPointWorld() / getHitNormalWorld() (return btVector3Array).
ClosestNotMeRayResultCallback(btCollisionObject me) — excludes a specific object from results.
All ray callbacks must be disposed. They extend BulletBase which wraps native memory.
Debug Drawing
DebugDrawer debugDrawer = new DebugDrawer();
debugDrawer.setDebugMode(btIDebugDraw.DebugDrawModes.DBG_DrawWireframe
| btIDebugDraw.DebugDrawModes.DBG_DrawContactPoints);
world.setDebugDrawer(debugDrawer);
debugDrawer.begin(camera);
world.debugDrawWorld();
debugDrawer.end();
debugDrawer.dispose();
DebugDrawer extends btIDebugDraw. Key debug mode flags:
| Flag | Value |
|---|
DBG_NoDebug | 0 |
DBG_DrawWireframe | 1 |
DBG_DrawAabb | 2 |
DBG_DrawContactPoints | 8 |
DBG_NoDeactivation | 16 |
DBG_DrawConstraints | 2048 |
DBG_DrawNormals | 16384 |
Memory Management
CRITICAL. Every bt* object wraps native C++ memory. The Java GC does NOT free it. You MUST call dispose() on every Bullet object you create.
Disposal Order (reverse of creation)
world.removeRigidBody(body);
body.dispose();
motionState.dispose();
constructionInfo.dispose();
shape.dispose();
contactListener.dispose();
world.dispose();
solver.dispose();
broadphase.dispose();
dispatcher.dispose();
collisionConfig.dispose();
Preventing GC of Java Wrappers
btCollisionObject maintains a static LongMap<btCollisionObject> instances keyed by native pointer. This keeps Java wrappers alive while Bullet references them. Disposal removes the entry.
userData is a Java-only Object field — invisible to native code. Use setUserValue(int) for data that must survive on the native side (e.g., entity array index for fast ContactListener lookups).
Reference Counting (optional)
Bullet.init(true);
shape.obtain();
shape.release();
btCompoundShape.addChildShape() auto-calls obtain() on child shapes when enabled.
Advanced Topics (brief)
Constraints: btPoint2PointConstraint, btHingeConstraint, btConeTwistConstraint, btSliderConstraint, btGeneric6DofConstraint, btGeneric6DofSpringConstraint, btGeneric6DofSpring2Constraint, btFixedConstraint. Add via world.addConstraint(constraint, disableCollisionBetweenBodies). Remove before disposing world.
Character controller: btKinematicCharacterController with btPairCachingGhostObject. Requires btGhostPairCallback registered on the broadphase's overlapping pair cache (broadphase.getOverlappingPairCache().setInternalGhostPairCallback(new btGhostPairCallback())). Works with any broadphase including btDbvtBroadphase. Added to world via world.addAction(controller).
Collision-only world: Use btCollisionWorld (3-arg constructor: dispatcher, broadphase, config — no solver) for collision detection without dynamics.
InternalTickCallback: Override onInternalTick(btDynamicsWorld, float timeStep) for per-substep callbacks. Attach as pre-tick or post-tick.
Soft bodies, vehicles: Available in the wrapper but advanced — consult the Bullet C++ documentation.
Common Mistakes
- Forgetting
Bullet.init() — Any bt* class instantiated before init() causes a segfault. Call it once in create().
- Not disposing
bt* objects — Native memory leak. Every bt* object, ray callback, MotionState, and ConstructionInfo needs dispose(). Java GC does NOT free the native allocation reliably.
- Wrong disposal order — Disposing the world while bodies are still in it, or disposing shapes while bodies reference them, causes crashes. Remove all bodies first, then dispose in reverse creation order.
- Skipping
calculateLocalInertia() for dynamic bodies — Body will have zero inertia and won't rotate or respond to off-center forces correctly. Always call shape.calculateLocalInertia(mass, outVector) when mass > 0.
- Using 1-arg
stepSimulation(delta) — Defaults maxSubSteps to 1. Any frame longer than 1/60s loses simulation time. Always use the 3-arg form.
- Forgetting
DISABLE_DEACTIVATION on kinematic bodies — Bullet deactivates the body and stops calling getWorldTransform(). The kinematic body appears frozen.
- Not resetting ray callbacks before reuse —
ClosestRayResultCallback retains state from the previous test. Must call setCollisionObject(null) and setClosestHitFraction(1f) before each rayTest().
- Disposing ConstructionInfo with shared MotionState/shape references —
btRigidBodyConstructionInfo does NOT take ownership. The MotionState and shape must outlive the body. Dispose the info early if you want, but keep the shape and MotionState alive.
- Expecting Box2D-style pixels-per-meter scaling — Bullet uses real-world units (meters). No PPM conversion needed. A 1-unit box is 1 meter.
- Sharing
btBvhTriangleMeshShape with dynamic bodies — Concave triangle mesh shapes are static-only. Use btConvexHullShape or btCompoundShape for dynamic bodies.
- Forgetting
CF_CUSTOM_MATERIAL_CALLBACK for onContactAdded — The callback silently never fires. At least one colliding body must have this flag set.
- Storing Java references only in
userData — This field is Java-only. For ContactListener performance, use setUserValue(int) to store an entity index, then look up your game object from an array.