| name | libgdx-box2d |
| description | Use when writing libGDX Java/Kotlin code involving Box2D physics — World creation/stepping, Body/BodyDef (Static/Kinematic/Dynamic), Fixture/FixtureDef, Shape types (PolygonShape/CircleShape/EdgeShape/ChainShape), collision filtering (categoryBits/maskBits/groupIndex), ContactListener callbacks, joints (Revolute/Distance/Prismatic/Weld/Mouse), Box2DDebugRenderer, pixels-to-meters conversion, or fixed timestep patterns. Use when debugging tunneling, bodies not colliding, crashes in contact callbacks, or sluggish physics movement. |
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;
Approach 2 — Camera in meters (cleaner):
camera = new OrthographicCamera();
camera.setToOrtho(false, viewportWidthMeters, viewportHeightMeters);
debugRenderer.render(world, camera.combined);
World
World world = new World(new Vector2(0, -10f), true);
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);
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);
world.setContactFilter(filter);
world.setGravity(new Vector2(0, -10f));
world.getGravity();
world.isLocked();
world.QueryAABB(callback, lowerX, lowerY, upperX, upperY);
world.rayCast(callback, point1, point2);
Array<Body> bodies = new Array<>();
world.getBodies(bodies);
world.dispose();
QueryCallback:
world.QueryAABB(fixture -> {
return true;
}, lowerX, lowerY, upperX, upperY);
RayCastCallback — method is reportRayFixture (NOT reportFixture):
world.rayCast((fixture, point, normal, fraction) -> {
return fraction;
}, startPoint, endPoint);
Body
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyDef.BodyType.DynamicBody;
bodyDef.position.set(x, y);
bodyDef.angle = 0;
bodyDef.fixedRotation = false;
bodyDef.bullet = false;
bodyDef.gravityScale = 1;
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;
fixtureDef.density = 1f;
fixtureDef.friction = 0.3f;
fixtureDef.restitution = 0.1f;
fixtureDef.isSensor = false;
Fixture fixture = body.createFixture(fixtureDef);
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);
poly.setAsBox(hx, hy, center, angleRadians);
poly.set(new float[]{x1,y1, x2,y2, x3,y3});
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;
fixtureDef.filter.categoryBits = PLAYER;
fixtureDef.filter.maskBits = ENEMY | WALL;
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);
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();
}
@Override public void endContact(Contact contact) { }
@Override public void preSolve(Contact contact, Manifold oldManifold) {
contact.setEnabled(false);
}
@Override public void postSolve(Contact contact, ContactImpulse impulse) {
float[] normals = impulse.getNormalImpulses();
int count = impulse.getCount();
}
});
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<>();
bodiesToDestroy.add(contact.getFixtureA().getBody());
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;
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);
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();
debugRenderer.render(world, camera.combined);
debugRenderer.render(world, camera.combined.cpy().scl(PPM));
debugRenderer.dispose();
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 to world.step() causes tunneling and jitter. Use the fixed-timestep accumulator pattern with 1/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 calling fixture.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() returning Array<Fixture>. getFixtures() does not exist.