Get the FREE Ultimate OpenClaw Setup Guide →

libgdx-bullet-physics

npx machina-cli add skill kyu-n/gdx-claude-skills/libgdx-bullet-physics --openclaw
Files (1)
SKILL.md
18.2 KB

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();  // MUST call before any bt* class. Loads native library. Segfault otherwise.

Three overloads:

Bullet.init()                                   // useRefCounting=false, logging=true
Bullet.init(boolean useRefCounting)              // logging=true
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));  // takes libGDX Vector3, NOT btVector3

Constructor: btDiscreteDynamicsWorld(btDispatcher, btBroadphaseInterface, btConstraintSolver, btCollisionConfiguration). Parameter order is dispatcher, broadphase, solver, config.

Stepping

// 3-arg form (recommended):
world.stepSimulation(delta, maxSubSteps, fixedTimeStep);
// delta: frame time; maxSubSteps: catch-up limit; fixedTimeStep: physics tick (1/60f typical)

// Return value: int — number of substeps performed
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);  // group/mask are int (bit flags)
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).

ShapeConstructorNotes
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);  // output parameter — writes into localInertia

On btCollisionShape. Must call for dynamic bodies (mass > 0). Static bodies use Vector3.Zero.

Rigid Bodies

Static vs Dynamic vs Kinematic

TypeMassInertiaFlagsMoved by
Static0Vector3.Zero(auto CF_STATIC_OBJECT)Nothing
Dynamic> 0calculateLocalInertia()(none needed)Physics
Kinematic0Vector3.ZeroCF_KINEMATIC_OBJECTApplication code via MotionState

Construction

Two patterns — both work. Direct construction bypasses the info object:

// Pattern 1: via ConstructionInfo (reusable for multiple bodies)
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();  // can dispose after body creation

// Pattern 2: direct
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);  // CRITICAL

Without DISABLE_DEACTIVATION, Bullet stops calling getWorldTransform() on the MotionState after the body sleeps — your kinematic body freezes.

Key btRigidBody Methods

// Forces (accumulated, applied at next step, cleared by clearForces)
body.applyCentralForce(Vector3 force);
body.applyForce(Vector3 force, Vector3 relPos);
body.applyTorque(Vector3 torque);
body.clearForces();

// Impulses (instant velocity change)
body.applyCentralImpulse(Vector3 impulse);
body.applyImpulse(Vector3 impulse, Vector3 relPos);

// Velocity
body.setLinearVelocity(Vector3 vel);
body.setAngularVelocity(Vector3 vel);

// Axis locking (e.g., 2D physics: lock Z axis)
body.setLinearFactor(new Vector3(1, 1, 0));
body.setAngularFactor(new Vector3(0, 0, 1));

// Damping
body.setDamping(float linearDamping, float angularDamping);

// Sleeping
body.setSleepingThresholds(float linear, float angular);
body.activate();  // wake up a sleeping body

btCollisionObject (base class)

body.setWorldTransform(Matrix4 transform);
body.getWorldTransform(Matrix4 out);    // output parameter form
Matrix4 t = body.getWorldTransform();   // allocating form

body.setUserValue(int value);           // int stored on native side — survives GC
int v = body.getUserValue();
body.userData = myObject;               // Java-only Object field — NOT on native side

body.setCollisionFlags(int flags);
body.setActivationState(int state);
body.isStaticOrKinematicObject();

Collision Flags (btCollisionObject.CollisionFlags)

ConstantValueUse
CF_STATIC_OBJECT1Auto-set for mass=0 bodies
CF_KINEMATIC_OBJECT2Must set manually for kinematic
CF_NO_CONTACT_RESPONSE4Trigger/sensor (detects but no physics response)
CF_CUSTOM_MATERIAL_CALLBACK8Required for onContactAdded
CF_CHARACTER_OBJECT16For character controllers

Activation States (com.badlogic.gdx.physics.bullet.collision.Collision)

ConstantValue
Collision.ACTIVE_TAG1
Collision.ISLAND_SLEEPING2
Collision.WANTS_DEACTIVATION3
Collision.DISABLE_DEACTIVATION4
Collision.DISABLE_SIMULATION5

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;  // share with ModelInstance.transform

    public MyMotionState(Matrix4 transform) {
        this.transform = transform;
    }

    @Override
    public void getWorldTransform(Matrix4 worldTrans) {
        worldTrans.set(transform);  // Bullet reads from your transform (kinematic)
    }

    @Override
    public void setWorldTransform(Matrix4 worldTrans) {
        transform.set(worldTrans);  // Bullet writes its result (dynamic)
    }
}

// Usage — share the ModelInstance's transform matrix:
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) {
        // match0/match1 = true if that object's flag matched the other's filter
        if (match0) { /* userValue0's object was matched */ }
        return true;
    }
};

Filtering setup:

body.setContactCallbackFlag(1 << 1);    // this object's identity bits
body.setContactCallbackFilter(1 << 1);  // which identity bits to listen for

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);
CallbackReturnsFires when
onContactAddedbooleanNew contact point created. Needs CF_CUSTOM_MATERIAL_CALLBACK.
onContactProcessedvoidContact processed during simulation step
onContactStartedvoidNew manifold between two objects
onContactEndedvoidManifold between two objects removed

Enable/disable individually: enableOnAdded() / disableOnAdded(), etc.

Raycasting

// Create once, reuse:
ClosestRayResultCallback callback = new ClosestRayResultCallback(Vector3.Zero, Vector3.Z);

// Before each ray test — MUST reset:
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);   // output parameter
    Vector3 hitNormal = new Vector3();
    callback.getHitNormalWorld(hitNormal);  // output parameter
}

// Dispose when done (NOT per-frame):
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();  // creates its own ShapeRenderer internally
debugDrawer.setDebugMode(btIDebugDraw.DebugDrawModes.DBG_DrawWireframe
    | btIDebugDraw.DebugDrawModes.DBG_DrawContactPoints);
world.setDebugDrawer(debugDrawer);

// In render(), after your normal 3D rendering:
debugDrawer.begin(camera);
world.debugDrawWorld();
debugDrawer.end();

// In dispose():
debugDrawer.dispose();

DebugDrawer extends btIDebugDraw. Key debug mode flags:

FlagValue
DBG_NoDebug0
DBG_DrawWireframe1
DBG_DrawAabb2
DBG_DrawContactPoints8
DBG_NoDeactivation16
DBG_DrawConstraints2048
DBG_DrawNormals16384

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)

// 1. Remove all bodies/objects from world
world.removeRigidBody(body);

// 2. Dispose bodies, motionStates, constructionInfos
body.dispose();
motionState.dispose();
constructionInfo.dispose();  // can dispose earlier, after body creation

// 3. Dispose collision shapes (only after all bodies using them are disposed)
shape.dispose();

// 4. Dispose contact listeners, debug drawer, ray callbacks
contactListener.dispose();

// 5. Dispose world infrastructure in reverse creation order
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);  // enable ref counting
shape.obtain();     // +1
shape.release();    // -1, auto-disposes at 0

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

  1. Forgetting Bullet.init() — Any bt* class instantiated before init() causes a segfault. Call it once in create().
  2. 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.
  3. 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.
  4. 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.
  5. Using 1-arg stepSimulation(delta) — Defaults maxSubSteps to 1. Any frame longer than 1/60s loses simulation time. Always use the 3-arg form.
  6. Forgetting DISABLE_DEACTIVATION on kinematic bodies — Bullet deactivates the body and stops calling getWorldTransform(). The kinematic body appears frozen.
  7. Not resetting ray callbacks before reuseClosestRayResultCallback retains state from the previous test. Must call setCollisionObject(null) and setClosestHitFraction(1f) before each rayTest().
  8. Disposing ConstructionInfo with shared MotionState/shape referencesbtRigidBodyConstructionInfo 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.
  9. Expecting Box2D-style pixels-per-meter scaling — Bullet uses real-world units (meters). No PPM conversion needed. A 1-unit box is 1 meter.
  10. Sharing btBvhTriangleMeshShape with dynamic bodies — Concave triangle mesh shapes are static-only. Use btConvexHullShape or btCompoundShape for dynamic bodies.
  11. Forgetting CF_CUSTOM_MATERIAL_CALLBACK for onContactAdded — The callback silently never fires. At least one colliding body must have this flag set.
  12. 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.

Source

git clone https://github.com/kyu-n/gdx-claude-skills/blob/master/skills/libgdx-bullet-physics/SKILL.mdView on GitHub

Overview

A practical guide to using the libGDX Bullet extension for 3D physics in Java/Kotlin. It covers the JNI-backed bt* objects, native memory management with dispose, and common patterns for initializing the world, stepping the simulation, and working with shapes, collision listeners, and debug drawing.

How This Skill Works

The extension exposes Bullet physics classes under com.badlogic.gdx.physics.bullet that wrap native C++ objects. All bt* objects allocate native memory and are not freed by the GC, so you must call dispose() when done. Initialize with Bullet.init(), build a btDiscreteDynamicsWorld, add shapes and bodies, then advance the simulation with stepSimulation.

When to Use It

  • Building 3D physics scenes in libGDX using btRigidBody, btDiscreteDynamicsWorld, and collision shapes
  • Debugging segfaults, native memory leaks, or bodies not moving in the Bullet wrapper
  • Troubleshooting collision detection failures or incorrect raycasting
  • Implementing a ContactListener, MotionState, and integrating debug drawing with libGDX Bullet
  • Managing physics tick timing with delta, substeps and fixed time steps

Quick Start

  1. Step 1: Call Bullet.init() before creating any Bullet objects
  2. Step 2: Setup the dynamics world: create collision config, dispatcher, broadphase, solver, then world and gravity
  3. Step 3: Add bodies, then call world.stepSimulation(delta, maxSubSteps, fixedTimeStep) and dispose when finished

Best Practices

  • Call Bullet.init() once in create() before using any Bullet classes
  • Dispose native bt* objects manually; GC won't free native memory
  • Use the 3-argument stepSimulation(delta, maxSubSteps, fixedTimeStep) form to keep physics in sync
  • Remove bodies from the world before disposing them; there is no removeAndDestroyBody
  • Use libGDX Vector3 for shapes and gravity rather than btVector3; Bullet is not HTML5/GWT compatible

Example Use Cases

  • Create a full dynamics world: btDefaultCollisionConfiguration, btCollisionDispatcher, btDbvtBroadphase, btSequentialImpulseSolver and btDiscreteDynamicsWorld; set gravity with a libGDX Vector3
  • Add a btRigidBody to the world and advance time with world.stepSimulation(delta, maxSubSteps, fixedTimeStep)
  • Create collision shapes like btBoxShape or btSphereShape using Vector3 or float parameters
  • Implement a ContactListener or debug drawer to visualize and detect collisions
  • Always dispose native Bullet objects when cleaning up to free memory

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers