Boks2D

Kotlin Multiplatform bindings for the Box2D physics engine.

Getting Started

The API closely mirrors Box2D v3, so existing knowledge of Box2D translates directly. The examples below cover the most common use cases to get you started.

World

The world is the root of the simulation. All bodies live inside it.

val world = World(WorldDef(gravity = Vec2(0f, -10f)))

world.step(1f / 60f, subStepCount = 4)

world.destroy() // always destroy when done

step() advances the simulation by one fixed timestep. Call it once per frame with a consistent value, typically 1/60 seconds. subStepCount controls accuracy vs performance (4 is a good default).

Bodies and Shapes

A body defines position, rotation, and movement. A shape defines the collision geometry and physical properties. Every body needs at least one shape to participate in collisions.

// static body - never moves (ground, walls, obstacles)
val ground = world.createBody(BodyDef(type = BodyType.Static))
ground.createPolygonShape(
ShapeDef(friction = 0.5f),
Polygon.makeBox(halfWidth = 10f, halfHeight = 0.5f)
)

// dynamic body - moved by the physics engine
val ball = world.createBody(BodyDef(
type = BodyType.Dynamic,
position = Vec2(0f, 10f)
))
ball.createCircleShape(
ShapeDef(density = 1f, friction = 0.3f, restitution = 0.6f),
Circle(center = Vec2.Zero, radius = 0.5f)
)

Available shape types: Circle, Polygon (box or arbitrary convex hull), Capsule, Segment.

Reading State

After each step(), read the body's position and angle to update your renderer.

world.step(1f / 60f, 4)

val position: Vec2 = ball.position // world-space center
val angle: Float = ball.angle // radians

Sleep

Box2D automatically puts idle bodies to sleep to save CPU. Sleeping bodies do not generate contact events. If you need events to fire continuously (e.g. in tests or always-on sensors), disable sleep:

// globally
world.isSleepingEnabled = false

// per body
ball.isAwake = true

Removing Bodies

It is safe to call world.destroyBody() any time after step() returns. The only place you must not destroy a body is inside callbacks that fire during the step (e.g. setPreSolveCallback), since the engine may still be processing that body on another thread.

world.step(1f / 60f, 4)

// safe! step() has already returned
bodies.removeAll { body ->
(body.position.y < -20f).also { if (it) world.destroyBody(body) }
}

Collision Events

Contact events are opt-in per shape for performance. Enable them in ShapeDef, then query after each step().

val ground = world.createBody(BodyDef(type = BodyType.Static))
ground.createPolygonShape(
ShapeDef(enableContactEvents = true),
Polygon.makeBox(10f, 0.5f)
)

val ball = world.createBody(BodyDef(type = BodyType.Dynamic, position = Vec2(0f, 5f)))
ball.createCircleShape(
ShapeDef(density = 1f, restitution = 0.7f, enableContactEvents = true, enableHitEvents = true),
Circle(Vec2.Zero, 0.5f)
)

world.step(1f / 60f, 4)

val events = world.getContactEvents()

// fired once when two shapes start touching
events.beginEvents.forEach { e -> println("contact begin: ${e.shapeA} / ${e.shapeB}") }

// fired once when they stop touching
events.endEvents.forEach { e -> println("contact end: ${e.shapeA} / ${e.shapeB}") }

// fired on significant impacts (speed > world.hitEventThreshold, default 1 m/s)
events.hitEvents.forEach { e ->
println("impact at ${e.point}, speed=${e.approachSpeed} m/s")
}

Note: endEvents shapes may have been destroyed. Always check shape.isValid before accessing them.

Note: Box2D suppresses restitution for low-speed impacts (restitutionThreshold, default 1 m/s). If a bouncing body stops reacting, lower the threshold.

Identifying objects in events

Events give you a Shape. Use shape.userData: Long to map shapes back to your game objects.

// assign an id when creating the shape
val shape = ball.createCircleShape(ShapeDef(enableContactEvents = true), Circle(Vec2.Zero, 0.5f))
shape.userData = myGameObject.id // any Long

// retrieve it in the event handler
val events = world.getContactEvents()
events.beginEvents.forEach { e ->
val objA = gameObjects[e.shapeA.userData]
val objB = gameObjects[e.shapeB.userData]
}

Collision Filtering

Control which shapes collide with which using category and mask bits. A collision happens only when both shapes accept each other:

collides = (A.categoryBits AND B.maskBits) ≠ 0
AND
(B.categoryBits AND A.maskBits) ≠ 0

categoryBits and maskBits are ULong (64 available categories). Use shl for readable bit definitions:

object Category {
val CHARACTER = 1uL shl 0 // ...0001
val OBSTACLE = 1uL shl 1 // ...0010
val PICKUP = 1uL shl 2 // ...0100
}

// character: collides with obstacles and pickups
val characterShape = characterBody.createCircleShape(
ShapeDef(filter = Filter(
categoryBits = Category.CHARACTER,
maskBits = Category.OBSTACLE or Category.PICKUP
)),
Circle(Vec2.Zero, 0.5f)
)

// obstacle: collides with characters only (obstacles don't push pickups around)
val obstacleShape = obstacleBody.createPolygonShape(
ShapeDef(filter = Filter(
categoryBits = Category.OBSTACLE,
maskBits = Category.CHARACTER
)),
Polygon.makeBox(1f, 1f)
)

// pickup (coin, power-up): collides with characters only
val pickupShape = pickupBody.createCircleShape(
ShapeDef(filter = Filter(
categoryBits = Category.PICKUP,
maskBits = Category.CHARACTER
)),
Circle(Vec2.Zero, 0.3f)
)

Packages

Link copied to clipboard
common
Link copied to clipboard
common
Link copied to clipboard
common
Link copied to clipboard
common
Link copied to clipboard
common
Link copied to clipboard
common