The RigidBody class provides a complete 2D physics simulation system that works seamlessly with the existing SAT collision detection system in Craters.
import { RigidBody, Vector } from 'craters';
// Create a dynamic body at position (100, 100) with mass 1
const dynamicBody = new RigidBody(
new Vector(100, 100), // position
1, // mass
0.5, // restitution (bounciness: 0-1)
0.3 // friction (0-1)
);
// Create a static body (immovable, like a wall)
const staticBody = new RigidBody(new Vector(0, 500), 0);
// or
const wall = new RigidBody(new Vector(0, 500), 1).setStatic(true);
Forces are accumulated and applied during the integration step:
// Apply gravity
const gravity = new Vector(0, 9.8);
body.applyForce(gravity);
// Apply a force at a specific point (creates torque/rotation)
const force = new Vector(10, 0);
const point = new Vector(120, 100); // Application point
body.applyForceAtPoint(force, point);
Impulses cause instant velocity changes (useful for jumps, explosions, etc.):
// Make the body jump
const jumpImpulse = new Vector(0, -500);
body.applyImpulse(jumpImpulse);
// Apply impulse at a point
const explosionImpulse = new Vector(100, -50);
const explosionPoint = new Vector(110, 110);
body.applyImpulseAtPoint(explosionImpulse, explosionPoint);
Call integrate() every frame with your delta time:
// In your game loop
const deltaTime = 1/60; // 60 FPS
body.integrate(deltaTime);
// Update the collision shape position to match the rigidbody
polygon.pos.copy(body.position);
polygon.setAngle(body.angle);
import { RigidBody, Vector, Polygon, Box, Response, testPolygonPolygon } from 'craters';
// Create a falling box
const boxBody = new RigidBody(new Vector(200, 100), 1, 0.7, 0.3);
const boxShape = new Box(new Vector(200, 100), 40, 40).toPolygon();
// Create a static floor
const floorBody = new RigidBody(new Vector(0, 400), 0);
const floorShape = new Box(new Vector(0, 400), 800, 40).toPolygon();
// Game loop
function update(dt) {
// Apply gravity
const gravity = new Vector(0, 980); // pixels/s²
boxBody.applyForce(gravity.clone().scale(boxBody.mass));
// Integrate physics
boxBody.integrate(dt);
// Update shape position
boxShape.pos.copy(boxBody.position);
boxShape.setAngle(boxBody.angle);
// Check collision
const response = new Response();
if (testPolygonPolygon(boxShape, floorShape, response)) {
// Resolve collision
boxBody.resolveCollision(floorBody, response);
// Update shape position after collision resolution
boxShape.pos.copy(boxBody.position);
}
}
// Run at 60 FPS
setInterval(() => update(1/60), 16.67);
// Super bouncy ball
const bouncyBall = new RigidBody(new Vector(100, 100), 0.5, 0.95, 0.1);
// Heavy, low-friction object (ice block)
const iceBlock = new RigidBody(new Vector(200, 100), 10, 0.2, 0.05);
// Sticky, low-restitution object
const stickyBox = new RigidBody(new Vector(300, 100), 1, 0.1, 0.9);
body.linearDamping = 0.98; // Less damping = slides farther
body.angularDamping = 0.95; // Less damping = spins longer
// No damping (space-like physics)
body.linearDamping = 1.0;
body.angularDamping = 1.0;
// Check if body is sleeping
if (body.isSleeping) {
console.log("Body is at rest");
}
// Wake up a sleeping body
body.wake();
// Manually put body to sleep
body.setSleeping(true);
// Customize sleep behavior
body.sleepThreshold = 0.005; // Lower = harder to sleep
body.sleepTimerMax = 120; // Frames before sleeping
// Set velocity directly
body.setVelocity(50, -100);
// Get current momentum
const momentum = body.getMomentum();
console.log(`Momentum: ${momentum.x}, ${momentum.y}`);
// Get kinetic energy
const energy = body.getKineticEnergy();
console.log(`Energy: ${energy}`);
The RigidBody manages physics, while shapes (Polygon, Circle) handle collision detection:
// Create body and shape separately
const body = new RigidBody(new Vector(100, 100), 1);
const shape = new Polygon(new Vector(100, 100), [
new Vector(-20, -20),
new Vector(20, -20),
new Vector(20, 20),
new Vector(-20, 20)
]);
// In update loop, sync shape with body
function syncShapeWithBody(body, shape) {
shape.pos.copy(body.position);
shape.setAngle(body.angle);
}
class Player {
body: RigidBody;
shape: Polygon;
grounded: boolean = false;
constructor(x: number, y: number) {
this.body = new RigidBody(new Vector(x, y), 1, 0, 0.5);
this.shape = new Box(new Vector(x, y), 32, 48).toPolygon();
}
jump() {
if (this.grounded) {
this.body.applyImpulse(new Vector(0, -600));
this.grounded = false;
}
}
moveLeft() {
this.body.applyForce(new Vector(-50, 0));
}
moveRight() {
this.body.applyForce(new Vector(50, 0));
}
update(dt: number) {
// Apply gravity
this.body.applyForce(new Vector(0, 980 * this.body.mass));
// Integrate
this.body.integrate(dt);
// Sync shape
this.shape.pos.copy(this.body.position);
// Check collisions with level geometry...
}
}
If you're using the Craters ECS system:
import { World } from 'craters';
// Create components
const PositionComponent = { x: 0, y: 0 };
const RigidBodyComponent = { body: null };
const ShapeComponent = { shape: null };
// Create entity
const world = new World();
const entity = world.createEntity();
world.addComponent(entity, 'position', { x: 100, y: 100 });
world.addComponent(entity, 'rigidbody', {
body: new RigidBody(new Vector(100, 100), 1)
});
world.addComponent(entity, 'shape', {
shape: new Box(new Vector(100, 100), 40, 40).toPolygon()
});
// Physics system
function physicsSystem(world, dt) {
const query = world.query(['rigidbody', 'shape']);
for (const entity of query) {
const rb = world.getComponent(entity, 'rigidbody').body;
const shape = world.getComponent(entity, 'shape').shape;
// Apply gravity
rb.applyForce(new Vector(0, 980 * rb.mass));
// Integrate
rb.integrate(dt);
// Sync shape with body
shape.pos.copy(rb.position);
shape.setAngle(rb.angle);
}
}
For stable physics, especially with gravity and stacking, it is highly recommended to use sub-stepping. Instead of updating physics once per frame with a large dt, update it multiple times with a smaller dt.
function update(dt: number) {
const subSteps = 4;
const subDt = dt / subSteps;
for (let i = 0; i < subSteps; i++) {
// 1. Apply Forces
// 2. Integrate (with subDt)
// 3. Collision Detection & Resolution
}
}
This reduces tunneling (objects passing through walls) and improves the stability of stacks.
new RigidBody(position?, mass?, restitution?, friction?)position: Vector - Current positionvelocity: Vector - Current velocitymass: number - Mass of the bodyrestitution: number - Bounciness (0-1)friction: number - Surface friction (0-1)isStatic: boolean - Whether body is immovableisSleeping: boolean - Whether body is sleepingsetStatic(isStatic: boolean): RigidBodyapplyForce(force: Vector): voidapplyForceAtPoint(force: Vector, point: Vector): voidapplyImpulse(impulse: Vector): voidapplyImpulseAtPoint(impulse: Vector, point: Vector): voidintegrate(dt: number): voidresolveCollision(other: RigidBody, response: Response): voidwake(): voidsetSleeping(sleeping: boolean): voidsetVelocity(x: number, y: number): RigidBodysetPosition(x: number, y: number): RigidBodysetMass(mass: number): RigidBodygetKineticEnergy(): numbergetMomentum(): Vector