Get the FREE Ultimate OpenClaw Setup Guide →

libgdx-box2d

Scanned
npx machina-cli add skill kyu-n/gdx-claude-skills/libgdx-box2d --openclaw
Files (1)
SKILL.md
13.3 KB

libGDX Box2D Physics

Quick reference for the libGDX Box2D JNI wrapper (com.badlogic.gdx.physics.box2d.*). Covers World, Body, Fixture, Shape, filtering, contacts, joints, debug rendering, and the PPM pattern.

Units — Box2D Works in Meters

Box2D is tuned for objects 0.1–10 meters. Using pixel coordinates causes sluggish movement (internal velocity cap is 2 units/timestep).

Approach 1 — PPM constant:

public static final float PPM = 100f; // 100 pixels = 1 meter

// Creating bodies:    bodyDef.position.set(pixelX / PPM, pixelY / PPM);
// Rendering sprites:  sprite.setPosition(body.getPosition().x * PPM, body.getPosition().y * PPM);
// Debug renderer:     debugRenderer.render(world, camera.combined.cpy().scl(PPM));

Approach 2 — Camera in meters (cleaner):

camera = new OrthographicCamera();
camera.setToOrtho(false, viewportWidthMeters, viewportHeightMeters);
// All game logic in meters. Debug renderer uses camera.combined directly.
debugRenderer.render(world, camera.combined);

World

World world = new World(new Vector2(0, -10f), true); // gravity, doSleep

Fixed timestep — the accumulator pattern (REQUIRED):

private static final float TIME_STEP = 1 / 60f;
private static final int VELOCITY_ITERATIONS = 6;
private static final int POSITION_ITERATIONS = 2;
private float accumulator = 0;

private void stepPhysics(float deltaTime) {
    float frameTime = Math.min(deltaTime, 0.25f); // cap to prevent spiral of death
    accumulator += frameTime;
    while (accumulator >= TIME_STEP) {
        world.step(TIME_STEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS);
        accumulator -= TIME_STEP;
    }
}

DO NOT pass Gdx.graphics.getDeltaTime() directly to world.step() — variable timestep causes tunneling and non-deterministic behavior.

Key methods:

world.setContactListener(listener);                // ContactListener
world.setContactFilter(filter);                    // ContactFilter — boolean shouldCollide(Fixture, Fixture)
world.setGravity(new Vector2(0, -10f));
world.getGravity();                                // returns REUSED Vector2 — copy if storing
world.isLocked();                                  // true during step — cannot modify world

// Queries — coordinates in meters (world units)
world.QueryAABB(callback, lowerX, lowerY, upperX, upperY);  // note: capital Q
world.rayCast(callback, point1, point2);                     // Vector2 or float overloads

// Bulk retrieval — clears array first, then populates
Array<Body> bodies = new Array<>();
world.getBodies(bodies);                           // NOT a return-value method

world.dispose();                                   // MUST call when done

QueryCallback:

world.QueryAABB(fixture -> {
    // return false to stop query, true to continue
    return true;
}, lowerX, lowerY, upperX, upperY);

RayCastCallback — method is reportRayFixture (NOT reportFixture):

world.rayCast((fixture, point, normal, fraction) -> {
    // point and normal are REUSED — copy if storing
    // Return: -1=ignore fixture, 0=terminate, fraction=clip to closest, 1=continue
    return fraction; // closest-hit
}, startPoint, endPoint);

Body

BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyDef.BodyType.DynamicBody; // StaticBody | KinematicBody | DynamicBody
bodyDef.position.set(x, y);                  // meters, NOT pixels
bodyDef.angle = 0;                           // radians, NOT degrees
bodyDef.fixedRotation = false;
bodyDef.bullet = false;                      // enable CCD for fast-moving bodies
bodyDef.gravityScale = 1;                    // per-body gravity multiplier (0 = no gravity)
Body body = world.createBody(bodyDef);

Also available on BodyDef: linearDamping, angularDamping (air resistance).

BodyTypeMoves?Responds to forces?Collides with
StaticBodyNoNoDynamic only
KinematicBodyYes (via velocity)No — use setLinearVelocity()Dynamic only
DynamicBodyYesYesAll types

Position: body.getPosition() (REUSED Vector2 — copy if storing!), body.getAngle() (radians), body.setTransform(x, y, angleRadians), body.getLinearVelocity() (REUSED), body.setLinearVelocity(vx, vy).

Forces (continuous, call every frame): applyForceToCenter(vec, wake), applyForce(vec, worldPoint, wake), applyTorque(torque, wake). Impulses (instantaneous): applyLinearImpulse(vec, worldPoint, wake), applyAngularImpulse(impulse, wake).

Other key methods: setUserData(obj) / getUserData(), getFixtureList() (NOT getFixtures()), setActive(false) (NOT setEnabled()), setSleepingAllowed(false) (NOT setAllowSleep()), setType(), setGravityScale(0).

Fixture & Shape

FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = shape;             // REQUIRED
fixtureDef.density = 1f;             // default: 0 (no mass)
fixtureDef.friction = 0.3f;          // default: 0.2
fixtureDef.restitution = 0.1f;       // default: 0 (bounciness)
fixtureDef.isSensor = false;         // sensors detect overlap but don't collide
Fixture fixture = body.createFixture(fixtureDef);
// Convenience: body.createFixture(shape, density);

CRITICAL: Dispose shapes after creating fixtures. Box2D clones the shape internally — your Shape leaks native memory if not disposed.

Shape Types

PolygonShape — max 8 vertices, must be convex, wound CCW:

PolygonShape poly = new PolygonShape();
poly.setAsBox(halfWidth, halfHeight);             // HALF-extents, NOT full size!
poly.setAsBox(hx, hy, center, angleRadians);      // offset + rotated
poly.set(new float[]{x1,y1, x2,y2, x3,y3});      // arbitrary convex polygon
poly.dispose();

CircleShape: setRadius(meters), setPosition(offsetVec2). EdgeShape: single line segment — use ChainShape for terrain instead. ChainShape: createChain(float[]) (open) or createLoop(float[]) (closed), prevents ghost collisions at seams. All shapes must be disposed.

Fixture methods: setUserData() / getUserData(), getBody(), getShape() (do NOT dispose — owned by Box2D), setSensor(), testPoint(worldX, worldY), setFilterData(filter) (MUST call to propagate changes), setDensity() (must call body.resetMassData() after).

Collision Filtering

Three fields on FixtureDef.filter (type short):

FieldDefaultPurpose
categoryBits0x0001What this fixture IS
maskBits-1 (0xFFFF)What this fixture COLLIDES WITH
groupIndex0Override: positive=always collide, negative=never

Algorithm (priority order):

  1. If both fixtures share the same non-zero groupIndex: positive = collide, negative = never
  2. Otherwise: (A.mask & B.category) != 0 && (B.mask & A.category) != 0 — both must accept each other

Example — 16 categories max (short = 16 bits):

static final short PLAYER = 0x0001;
static final short ENEMY  = 0x0002;
static final short BULLET = 0x0004;
static final short WALL   = 0x0008;

// Player collides with enemies and walls, not own bullets
fixtureDef.filter.categoryBits = PLAYER;
fixtureDef.filter.maskBits = ENEMY | WALL;

// Bullet collides with enemies and walls only
fixtureDef.filter.categoryBits = BULLET;
fixtureDef.filter.maskBits = ENEMY | WALL;

Runtime filter changes:

Filter filter = new Filter();
filter.categoryBits = BULLET;
filter.maskBits = WALL;
fixture.setFilterData(filter); // MUST call — modifying getFilterData() fields without this is undefined

Use -1 for maskBits (collide with everything), not (short)0xFFFF. Both are equivalent but -1 is idiomatic and avoids the cast.

ContactListener

world.setContactListener(new ContactListener() {
    @Override public void beginContact(Contact contact) {
        Fixture a = contact.getFixtureA();
        Fixture b = contact.getFixtureB();
        // Order is NOT guaranteed — check both directions
    }
    @Override public void endContact(Contact contact) { }
    @Override public void preSolve(Contact contact, Manifold oldManifold) {
        // Can disable contact for this step (one-way platforms):
        contact.setEnabled(false);
    }
    @Override public void postSolve(Contact contact, ContactImpulse impulse) {
        float[] normals = impulse.getNormalImpulses(); // float[2]
        int count = impulse.getCount();                // how many points valid
    }
});

CRITICAL: Do NOT create/destroy bodies or joints inside ANY callback. Box2D is mid-step (world.isLocked() == true). Queue changes and apply after world.step():

private Array<Body> bodiesToDestroy = new Array<>();

// In ContactListener:
bodiesToDestroy.add(contact.getFixtureA().getBody());

// After world.step():
for (Body b : bodiesToDestroy) {
    world.destroyBody(b);
}
bodiesToDestroy.clear();

Joints

All joints share: JointDef.bodyA, JointDef.bodyB, JointDef.collideConnected (default false). Create with world.createJoint(def) — cast result to specific joint type.

MouseJoint — drag body to point (most common, no initialize()):

MouseJointDef def = new MouseJointDef();
def.bodyA = groundBody;       // static body (required anchor)
def.bodyB = draggedBody;
def.target.set(touchWorldX, touchWorldY);
def.maxForce = 1000f * draggedBody.getMass();
def.frequencyHz = 5f;
def.dampingRatio = 0.7f;
MouseJoint joint = (MouseJoint) world.createJoint(def);
// Update target each frame: joint.setTarget(newWorldPoint);

Other joints — all use def.initialize(bodyA, bodyB, ...) unless noted:

  • RevoluteJoint (pin/hinge): initialize(bodyA, bodyB, worldAnchor). Supports enableMotor/motorSpeed/maxMotorTorque, enableLimit/lowerAngle/upperAngle.
  • DistanceJoint (spring/rod): initialize(bodyA, bodyB, anchorA, anchorB). Uses frequencyHz/dampingRatio (NOT stiffness/damping).
  • PrismaticJoint (slider): initialize(bodyA, bodyB, worldAnchor, axisDirection). Supports motor and translation limits.
  • WeldJoint (glue): initialize(bodyA, bodyB, worldAnchor). frequencyHz=0 for rigid (may flex under stress).
  • RopeJoint (max distance): no initialize() — set bodyA/bodyB/localAnchorA/localAnchorB/maxLength directly.

Destroying joints: world.destroyJoint(joint). Destroying a body auto-destroys its joints — do NOT destroy the joint separately afterward.

Box2DDebugRenderer

Box2DDebugRenderer debugRenderer = new Box2DDebugRenderer();
// Or: new Box2DDebugRenderer(drawBodies, drawJoints, drawAABBs,
//                             drawInactiveBodies, drawVelocities, drawContacts);

// In render():
debugRenderer.render(world, camera.combined); // if camera is in meters
// OR with PPM:
debugRenderer.render(world, camera.combined.cpy().scl(PPM));

debugRenderer.dispose(); // MUST dispose — uses internal ShapeRenderer

Common Mistakes

  1. Using pixel coordinates in Box2D — Bodies at (400, 300) pixels behave as 400-meter objects. Use meters (typically 0.1–10 range). Convert with PPM constant or use a camera in meter units.
  2. Variable timestep — Passing getDeltaTime() directly to world.step() causes tunneling and jitter. Use the fixed-timestep accumulator pattern with 1/60f.
  3. Destroying bodies in ContactListener — Crashes with native error. World is locked during callbacks. Queue bodies and destroy after world.step().
  4. Forgetting shape.dispose() after createFixture() — Box2D clones the shape data. Your Java Shape still holds a native pointer that leaks if not disposed.
  5. setAsBox(width, height) with full dimensionsPolygonShape.setAsBox() takes half-extents. setAsBox(2, 3) creates a 4x6 meter box.
  6. Storing body.getPosition() reference — Returns a REUSED Vector2 that changes on every call. Copy the values: new Vector2(body.getPosition()).
  7. Not checking both fixtures in ContactListenergetFixtureA()/getFixtureB() order is arbitrary. Always check both directions when identifying collision pairs.
  8. Applying forces to KinematicBody — Kinematic bodies ignore forces and impulses entirely. Control them with setLinearVelocity() / setTransform().
  9. Using EdgeShape for terrain — Adjacent EdgeShapes cause ghost collisions at seams. Use ChainShape instead.
  10. Modifying filter without setFilterData() — Getting fixture.getFilterData() and changing fields without calling fixture.setFilterData(filter) does not propagate to native Box2D.
  11. PolygonShape with >8 vertices or concave shape — Box2D silently fails or crashes. Max 8 vertices, must be convex, wound counter-clockwise.
  12. Calling getFixtures() instead of getFixtureList() — The method is body.getFixtureList() returning Array<Fixture>. getFixtures() does not exist.

Source

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

Overview

This guide covers the libGDX Box2D JNI wrapper and all core physics concepts: World setup and stepping, Body/Fixture/Shape creation (Static/Kinematic/Dynamic), collision filtering, contact callbacks, and joints. It also explains pixels-to-meters conversion, debug rendering with Box2DDebugRenderer, and the fixed-timestep (accumulator) pattern to prevent tunneling and jitter.

How This Skill Works

Box2D operates in meters, so convert pixels via a PPM constant or use a meters-based camera. Physics stepping uses a fixed TIME_STEP with an accumulator; cap deltaTime to avoid instability and loop world.step(TIME_STEP, velocityIterations, positionIterations) until drained. You wire up a ContactListener, a ContactFilter, and use QueryAABB and rayCast callbacks; render with Box2DDebugRenderer for visualization.

When to Use It

  • Debug tunneling, missed collisions, or crashes in contact callbacks
  • Initialize World and bodies (Static/Kinematic/Dynamic) and configure shapes/fixtures
  • Configure collision filtering (categoryBits/maskBits/groupIndex) and implement ContactListener callbacks
  • Create and tune joints (Revolute/Distance/Prismatic/Weld/Mouse) and use Box2DDebugRenderer for visualization
  • Adopt the fixed timestep and PPM pattern to ensure deterministic stepping and stable motion

Quick Start

  1. Step 1: Create a World and set a gravity vector, then attach a ContactListener
  2. Step 2: Build BodyDefs, FixtureDefs and Shapes (e.g., Dynamic with CircleShape or PolygonShape) and create bodies
  3. Step 3: Run a loop that uses a fixed TIME_STEP accumulator to call world.step and render with a Box2DDebugRenderer

Best Practices

  • Use a single, shared PPM constant and convert positions for rendering, physics, and debug rendering
  • Implement the accumulator-based fixed timestep (TIME_STEP = 1/60f) and clamp frameTime (e.g., 0.25f) before stepping
  • Avoid modifying the world while it is locked; check world.isLocked() and batch edits
  • Reuse Vector2 and callback instances in tight loops (QueryAABB, RayCast) to reduce garbage
  • Call world.dispose() when the game ends and clean up resources

Example Use Cases

  • Spawn a Dynamic CircleShape body for the player and a Static ground body, with collision filtering to ignore certain objects
  • Use a RevoluteJoint to create a swinging door and a Mouse joint to drag bodies in debug mode
  • Render physics with Box2DDebugRenderer and synchronize camera in meters to avoid desynchronization
  • Convert sprite positions to meters using a 100 PPM conversion and render at body.getPosition() * PPM
  • Implement a fixed-timestep loop in the game loop and step the world with accumulator rather than Gdx.graphics.getDeltaTime()

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers