Skip to content

Physics

YAGE uses Rapier2D under the hood for physics simulation. The @yagejs/physics package wraps Rapier with a pixel-based API — you never need to think about meters. All positions, velocities, and forces are in pixels.

import { PhysicsPlugin } from "@yagejs/physics";
engine.use(new PhysicsPlugin({
gravity: { x: 0, y: 980 }, // pixels/s², default: (0, 980)
pixelsPerMeter: 50, // conversion factor, default: 50
}));

RigidBodyComponent wraps a Rapier rigid body. Add it after Transform.

import { RigidBodyComponent } from "@yagejs/physics";
entity.add(new RigidBodyComponent({
type: "dynamic", // "dynamic" | "static" | "kinematic"
fixedRotation: true, // prevent rotation
gravityScale: 0, // ignore gravity (0 = zero-g, 1 = normal)
linearDamping: 5, // velocity drag
angularDamping: 1, // rotation drag
ccd: true, // continuous collision detection for fast objects
lockTranslationX: false, // lock horizontal movement
syncRotation: true, // sync physics rotation back to Transform
}));

Body types:

  • dynamic — affected by forces, gravity, and collisions. Use for players, projectiles, and physics objects.
  • static — never moves. Use for walls, floors, and platforms.
  • kinematic — moved programmatically, not by forces. Use for moving platforms, elevators, and doors. Move kinematic bodies by setting the Transform position (e.g. transform.setPosition(x, y)), not by calling rb.setPosition — the physics system automatically syncs Transform → Rapier each frame before stepping. rb.setPosition is a teleport intended for dynamic bodies (e.g. respawning a player).

ColliderComponent defines the collision shape. Add it after RigidBodyComponent.

The required component order is: TransformRigidBodyComponentColliderComponent.

import { ColliderComponent } from "@yagejs/physics";
// Box
entity.add(new ColliderComponent({
shape: { type: "box", width: 64, height: 32 },
restitution: 0.5, // bounciness (0–1)
friction: 0.3,
density: 1, // affects mass for dynamic bodies
}));
// Circle
entity.add(new ColliderComponent({
shape: { type: "circle", radius: 16 },
}));
// Capsule
entity.add(new ColliderComponent({
shape: { type: "capsule", halfHeight: 20, radius: 10 },
}));
// Convex polygon
entity.add(new ColliderComponent({
shape: {
type: "polygon",
vertices: [{ x: 0, y: -20 }, { x: 20, y: 20 }, { x: -20, y: 20 }],
},
}));

All dimensions are in pixels.

CollisionLayers lets you control which objects can collide using named layer bitmasks.

import { CollisionLayers } from "@yagejs/physics";
const layers = new CollisionLayers();
const LAYER_PLAYER = layers.define("player");
const LAYER_WALL = layers.define("wall");
const LAYER_COIN = layers.define("coin");
// Player collides with walls and coins
entity.add(new ColliderComponent({
shape: { type: "circle", radius: 16 },
layers: LAYER_PLAYER,
mask: LAYER_WALL | LAYER_COIN,
}));
// Coin only collides with player
coinEntity.add(new ColliderComponent({
shape: { type: "circle", radius: 10 },
sensor: true,
layers: LAYER_COIN,
mask: LAYER_PLAYER,
}));

An object on layer A with mask B will only interact with objects on layer B that also have A in their mask.

A sensor collider detects overlaps without producing a physical response. Use sensors for trigger zones, collectibles, and hit detection.

const collider = new ColliderComponent({
shape: { type: "circle", radius: 10 },
sensor: true,
});
entity.add(collider);
collider.onTrigger((event) => {
if (event.entered) {
console.log("entered by", event.other.name);
} else {
console.log("exited by", event.other.name);
}
// event.otherCollider — the other entity's ColliderComponent
});

For non-sensor colliders, use onCollision instead:

collider.onCollision((event) => {
if (event.started) {
console.log("hit", event.other.name);
console.log("contact normal:", event.contactNormal);
console.log("contact point:", event.contactPoint);
}
});

Both handlers return an unsubscribe function.

You can query which entities currently overlap a sensor collider:

const overlapping = collider.getOverlapping();
const enemies = collider.getOverlapping({ has: [EnemyTag] });
const components = collider.getOverlappingComponents(HealthComponent);

Prefer setVelocity for direct control. Use applyImpulse or applyForce for physics-driven movement.

const rb = entity.get(RigidBodyComponent);
// Direct velocity control (px/s) — best for player movement
rb.setVelocity(new Vec2(200, 0));
rb.setVelocityX(200); // set X, keep Y
rb.setVelocityY(-300); // set Y, keep X
// Read velocity
const vel = rb.getVelocity(); // Vec2 in px/s
// Impulse (instant momentum change)
rb.applyImpulse(new Vec2(0, -4000));
// Force (continuous, applied per frame)
rb.applyForce(new Vec2(100, 0));
// Rotation
rb.setAngularVelocity(Math.PI);
rb.applyTorque(50);

Use setPosition to teleport a body without interpolation artifacts:

rb.setPosition(400, 300); // pixels

Cast a ray to find the first entity hit:

const world = this.use(PhysicsWorldManagerKey).getOrCreateWorld(this.scene);
const hit = world.raycast(
origin, // Vec2Like — start position in pixels
{ x: 0, y: 1 }, // direction (normalized)
100, // max distance in pixels
{ filterGroups: CollisionLayers.interactionGroups(LAYER_PLAYER, LAYER_WALL) },
);
if (hit) {
console.log(hit.entity); // Entity that was hit
console.log(hit.point); // Vec2 — hit position in pixels
console.log(hit.normal); // Vec2 — surface normal
console.log(hit.distance); // number — distance in pixels
}

A common use case is ground detection for platformers:

const grounded = world.raycast(
this.transform.worldPosition,
{ x: 0, y: 1 },
playerHeight / 2 + 2,
) !== null;

Change gravity at runtime via the PhysicsWorld:

const worldMgr = this.use(PhysicsWorldManagerKey);
const world = worldMgr.getOrCreateWorld(this.scene);
world.setGravity(0, -980); // flip gravity upward
world.setGravity(0, 0); // zero gravity

Per-body gravity scaling is set via gravityScale in RigidBodyConfig.

Lock or unlock axes at runtime:

rb.setEnabledTranslations(true, false); // lock Y axis
rb.lockRotations(true); // lock rotation

Enable CCD on fast-moving bodies to prevent tunneling through thin colliders:

entity.add(new RigidBodyComponent({
type: "dynamic",
ccd: true,
}));

CCD adds a small performance cost — only enable it for bullets, fast projectiles, or small objects that move at high speed.

Access the physics world from any component:

import { PhysicsWorldManagerKey } from "@yagejs/physics";
const worldMgr = this.use(PhysicsWorldManagerKey);
const world = worldMgr.getOrCreateWorld(this.scene);

Each scene gets its own physics world. The PhysicsWorldManager creates and manages worlds per-scene automatically.