-
-
Notifications
You must be signed in to change notification settings - Fork 664
Migrating to the Physics Adapter API
melonJS 19.5 introduces the PhysicsAdapter interface — a portable physics layer that lets game code work on the built-in SAT physics (default), @melonjs/matter-adapter, or @melonjs/planck-adapter (faithful Box2D 2.3.0 port). This page covers migrating from the legacy new me.Body(this, shape) pattern to the new declarative bodyDef API, all on the built-in adapter. No engine swap, no behavioural change — just a code-style migration that prepares your game to switch engines later if you want to.
The legacy API still works on the built-in adapter and is not slated for removal. You only need to migrate if:
- You plan to switch to
@melonjs/matter-adapteror@melonjs/planck-adapter(or a future Rapier adapter) - You want your game code to be portable across adapters
- You want to use the new collision lifecycle hooks (
onCollisionStart/onCollisionActive/onCollisionEnd) — note these are not drop-in replacements for the legacyonCollision; they have an intentionally different contract (see the section below)
If none of those apply, your existing code keeps working unchanged.
import { Body, collision, Rect, Sprite } from "melonjs";
class Player extends Sprite {
constructor(x, y) {
super(x, y, { image: "player", framewidth: 64, frameheight: 96 });
this.body = new Body(this, new Rect(0, 0, 64, 96));
this.body.setMaxVelocity(3, 15);
this.body.setFriction(0.4, 0);
this.body.collisionType = collision.types.PLAYER_OBJECT;
this.body.collisionMask =
collision.types.WORLD_SHAPE | collision.types.ENEMY_OBJECT;
}
}import { collision, Rect, Sprite } from "melonjs";
class Player extends Sprite {
constructor(x, y) {
super(x, y, { image: "player", framewidth: 64, frameheight: 96 });
// declarative body — auto-registered with the active adapter when
// the renderable is added to a container (Container.addChild).
this.bodyDef = {
type: "dynamic",
shapes: [new Rect(0, 0, 64, 96)],
collisionType: collision.types.PLAYER_OBJECT,
collisionMask:
collision.types.WORLD_SHAPE | collision.types.ENEMY_OBJECT,
maxVelocity: { x: 3, y: 15 },
frictionAir: { x: 0.4, y: 0 },
};
}
}A few things to notice:
-
Bodyno longer needs to be imported. - All body configuration lives in one declarative object, not spread across post-construction setters.
- The body is constructed by the adapter when the renderable is added to a container, so
this.bodybecomes available afterContainer.addChildruns (typically insideonActivateEventor the next frame).
update(dt) {
if (input.isKeyPressed("left")) {
this.body.force.x = -this.body.maxVel.x;
} else if (input.isKeyPressed("right")) {
this.body.force.x = this.body.maxVel.x;
}
if (input.isKeyPressed("jump") && !this.body.jumping) {
this.body.vel.y = -this.body.maxVel.y;
this.body.jumping = true;
}
return super.update(dt);
}update(dt) {
const vel = this.body.getVelocity();
if (input.isKeyPressed("left")) {
this.body.applyForce(-this.body.maxVel.x, 0);
} else if (input.isKeyPressed("right")) {
this.body.applyForce(this.body.maxVel.x, 0);
}
if (input.isKeyPressed("jump") && !this.body.jumping) {
this.body.setVelocity(vel.x, -this.body.maxVel.y);
this.body.jumping = true;
}
return super.update(dt);
}Direct body.vel.x = ... and body.force.x = ... mutation still works on the built-in adapter (backward compatibility). The body methods (setVelocity, applyForce) are equivalent and portable — under @melonjs/matter-adapter, the same calls delegate to Matter.Body.setVelocity and Matter.Body.applyForce for you.
Note that applyForce is accumulating (each call adds to the per-step force), matching matter's semantics. Multiple applyForce calls in the same frame stack; the engine clears the accumulator at end-of-step. If you want set-and-replace semantics, body.force.set(x, y) is still available on the built-in adapter — but it's not portable.
onCollision(response, other) {
if (other.body.collisionType === collision.types.ENEMY_OBJECT) {
if (response.overlapV.y > 0 && !this.body.falling) {
// stomp
this.body.vel.y = -8;
return false; // skip the SAT push-out so we don't bump back
}
this.hurt();
}
return false;
}// onCollisionStart fires exactly once when contact begins — fits a stomp /
// pickup / trigger-entry event. The legacy `onCollision` still works on
// every adapter and fires every frame the bodies overlap; pick whichever
// cadence fits your handler logic.
onCollisionStart(response, other) {
if (other.body.collisionType === collision.types.ENEMY_OBJECT) {
const vel = this.body.getVelocity();
if (vel.y > 0) {
// I was falling at the moment of impact — stomp
this.body.setVelocity(vel.x, -8);
return;
}
this.hurt();
}
}This is the most important migration nuance on the page. The legacy onCollision handler is kept unchanged for backward compatibility — it still works exactly like it did in 19.4 — and the new onCollisionStart / onCollisionActive / onCollisionEnd lifecycle handlers introduced in 19.5 have an intentionally different contract. They live side by side; you can use either.
onCollision (legacy, unchanged) |
onCollisionStart / onCollisionActive / onCollisionEnd (new) |
|
|---|---|---|
| Cadence for dynamic-dynamic pairs | 2× per frame per side | 1× per frame per side (deduplicated) |
response.a semantics |
Fixed per pair (first body in detector call) | Always the receiver (response.a === this) |
response.b semantics |
Fixed per pair | Always the partner (response.b === other) |
response.overlapN / response.overlapV
|
"from a → b" (SAT legacy convention) | Flipped per receiver so the convention is "from me → other" |
response.normal |
✗ | ✓ — MTV of receiver. normal.y < -0.7 means "push me up" ⇒ I'm on top of other
|
response.depth |
✗ | ✓ — penetration depth (matter-native name; aliases overlap semantically) |
return false to skip the SAT push-out |
✓ (legacy idiom honored) | ✗ — use bodyDef.isSensor: true or setSensor(true) instead |
| Engine portability | Builtin only (matter ignores the return value) | Identical contract on every adapter |
Migration guidance:
- Staying on
onCollisionis fine if you don't want any behavior change — that's the entire point of keeping it. New code in 19.5 can absolutely useonCollision. - Move to
onCollisionStart/onCollisionActivewhen you want the cleaner contract: simpler stomp idiom (response.normal.y < -0.7), no dispatch ordering surprises, and code that runs identically on every adapter.
Supersedes rule (per renderable): if a renderable defines onCollisionActive, the legacy onCollision is not dispatched on that renderable — they are the same every-frame handler in two API styles, and dispatching both for the same overlap with different response shapes would be a footgun. Per-side, so renderable A can be on the modern handler while renderable B is on the legacy one in the same pair; each gets the contract it asked for.
Push-out default (matter-aligned): with no onCollision defined, push-out happens by default for dynamic non-sensor bodies on both adapters. Migrating from onCollision to onCollisionActive does not silently make your bodies pass through each other — solid stays solid. To opt out of push-out, mark the body as a sensor (bodyDef.isSensor: true or body.setSensor(true)). The pre-19.5 "no onCollision defined ⇒ implicit sensor" quirk is gone; the explicit flag replaces it.
-
Velocity check (
vel.y > 0) replacesresponse.overlapV.y > 0. The pre-contact velocity signal is identical on every adapter; the response object's shape underonCollisionis engine-specific (SAT exposesoverlapV/overlapN, matter exposesnormal/depth/pair— see Switching Physics Adapters). -
return falsefromonCollisionto skip the SAT push-out for one-way platforms or triggers: the portable equivalent isbodyDef.isSensor: true(orbody.setSensor(true)at runtime).TriggerandCollectablealready declare themselves as sensors in 19.5+.
| Legacy (≤19.4) | Adapter API (19.5+) | Notes |
|---|---|---|
this.body = new Body(this, shape) |
this.bodyDef = { type: "dynamic", shapes: [shape], … } |
Auto-registered on Container.addChild
|
body.setMaxVelocity(x, y) |
bodyDef.maxVelocity: { x, y } |
|
body.setFriction(x, y) |
bodyDef.frictionAir: { x, y } |
Renamed to match matter; scalar form also accepted |
body.setStatic(true) |
bodyDef.type: "static" |
body.setStatic(true) still works at runtime |
body.bounce = X |
bodyDef.restitution: X |
Renamed to match matter |
body.mass = X |
bodyDef.density: X |
Built-in: 1:1 with mass. Matter: density × shape area. See BuiltinAdapter Quirks #4
|
body.gravityScale = X |
bodyDef.gravityScale: X |
|
body.ignoreGravity = true |
bodyDef.gravityScale: 0 |
Same effect, portable |
body.collisionType = X |
bodyDef.collisionType: X |
|
body.collisionMask = X |
bodyDef.collisionMask: X |
|
body.vel.set(x, y) |
body.setVelocity(x, y) |
Direct body.vel.x = … still works on built-in |
body.force.x = X; body.force.y = Y |
body.applyForce(x, y) |
Accumulates per step; matches matter's semantics. body.force.set(...) still works on built-in |
body.isSensor = true |
body.setSensor(true) or bodyDef.isSensor: true
|
|
onCollision(response, other) |
(still onCollision) or onCollisionStart / onCollisionActive — see callback section; these are NOT drop-in equivalents |
Legacy onCollision kept unchanged; new handlers have a different contract |
return false from onCollision
|
bodyDef.isSensor: true |
Only needed when migrating away from onCollision. The legacy handler still honors return false on Builtin. |
If you're staying on the built-in adapter, the only practical benefit of migrating is access to the new collision lifecycle hooks (onCollisionStart, onCollisionEnd) and the body method API (body.setVelocity, body.applyForce, etc.). The legacy patterns continue to work indefinitely.
The real motivation is forward portability. The adapter-API form is the same on every physics engine. If you ever want to try @melonjs/matter-adapter or @melonjs/planck-adapter for a specific feature — constraints / joints, sleeping bodies, continuous collision detection, native rotational dynamics — the migration is a single line in new Application(…) rather than a full code rewrite.
- Switching Physics Adapters — once on the adapter API, this is the page for actually swapping built-in for matter (or another future adapter).
- BuiltinAdapter Quirks — SAT-specific behaviors that don't carry to other engines, useful if you're planning to switch.
- Adding a Physic Body to a Renderable — general primer on physics bodies in melonJS.