libgdx-box2d
Scannednpx machina-cli add skill kyu-n/gdx-claude-skills/libgdx-box2d --openclawlibGDX 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).
| BodyType | Moves? | Responds to forces? | Collides with |
|---|---|---|---|
| StaticBody | No | No | Dynamic only |
| KinematicBody | Yes (via velocity) | No — use setLinearVelocity() | Dynamic only |
| DynamicBody | Yes | Yes | All 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):
| Field | Default | Purpose |
|---|---|---|
categoryBits | 0x0001 | What this fixture IS |
maskBits | -1 (0xFFFF) | What this fixture COLLIDES WITH |
groupIndex | 0 | Override: positive=always collide, negative=never |
Algorithm (priority order):
- If both fixtures share the same non-zero
groupIndex: positive = collide, negative = never - 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). SupportsenableMotor/motorSpeed/maxMotorTorque,enableLimit/lowerAngle/upperAngle. - DistanceJoint (spring/rod):
initialize(bodyA, bodyB, anchorA, anchorB). UsesfrequencyHz/dampingRatio(NOT stiffness/damping). - PrismaticJoint (slider):
initialize(bodyA, bodyB, worldAnchor, axisDirection). Supports motor and translation limits. - WeldJoint (glue):
initialize(bodyA, bodyB, worldAnchor).frequencyHz=0for rigid (may flex under stress). - RopeJoint (max distance): no
initialize()— setbodyA/bodyB/localAnchorA/localAnchorB/maxLengthdirectly.
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
- 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. - Variable timestep — Passing
getDeltaTime()directly toworld.step()causes tunneling and jitter. Use the fixed-timestep accumulator pattern with1/60f. - Destroying bodies in ContactListener — Crashes with native error. World is locked during callbacks. Queue bodies and destroy after
world.step(). - Forgetting shape.dispose() after createFixture() — Box2D clones the shape data. Your Java Shape still holds a native pointer that leaks if not disposed.
- setAsBox(width, height) with full dimensions —
PolygonShape.setAsBox()takes half-extents.setAsBox(2, 3)creates a 4x6 meter box. - Storing body.getPosition() reference — Returns a REUSED Vector2 that changes on every call. Copy the values:
new Vector2(body.getPosition()). - Not checking both fixtures in ContactListener —
getFixtureA()/getFixtureB()order is arbitrary. Always check both directions when identifying collision pairs. - Applying forces to KinematicBody — Kinematic bodies ignore forces and impulses entirely. Control them with
setLinearVelocity()/setTransform(). - Using EdgeShape for terrain — Adjacent EdgeShapes cause ghost collisions at seams. Use ChainShape instead.
- Modifying filter without setFilterData() — Getting
fixture.getFilterData()and changing fields without callingfixture.setFilterData(filter)does not propagate to native Box2D. - PolygonShape with >8 vertices or concave shape — Box2D silently fails or crashes. Max 8 vertices, must be convex, wound counter-clockwise.
- Calling getFixtures() instead of getFixtureList() — The method is
body.getFixtureList()returningArray<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
- Step 1: Create a World and set a gravity vector, then attach a ContactListener
- Step 2: Build BodyDefs, FixtureDefs and Shapes (e.g., Dynamic with CircleShape or PolygonShape) and create bodies
- 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()