-
-
Notifications
You must be signed in to change notification settings - Fork 662
Migrating to the Physics Adapter API
melonJS 19.5 introduces the PhysicsAdapter interface — a portable physics layer that lets game code work on either the built-in SAT physics (default) or @melonjs/matter-adapter. 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-adapter(or a future Box2D / Rapier adapter) - You want your game code to be portable across adapters
- You want to use the new collision lifecycle hooks (
onCollisionStart/onCollisionEnd) — though the legacyonCollisionis still dispatched and remains the right choice when "every frame the bodies overlap" semantics fit
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();
}
}Two changes worth calling out:
-
onCollision→onCollisionStart/onCollisionActive/onCollisionEndis now the recommended lifecycle.onCollisionis the legacy alias foronCollisionActive(every-frame) and continues to work on every adapter — keep it if its semantics fit; switch when one-shot semantics fit better. -
Velocity check (
vel.y > 0) replacesresponse.overlapV.y > 0. The pre-contact velocity signal is identical on every adapter; the response object's shape is engine-specific (SAT exposesoverlapV/overlapN, matter exposesnormal/depth/pair— see Switching Physics Adapters).
If you used return false from onCollision to skip the SAT push-out for one-way platforms or triggers, the portable equivalent is bodyDef.isSensor: true (or body.setSensor(true) at runtime). Trigger and Collectable already 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) |
onCollisionStart (one-shot) or onCollisionActive (every frame) |
Legacy onCollision still dispatched |
return false from onCollision
|
bodyDef.isSensor: true |
Or adapter.setSensor(other, true) at runtime |
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 for a specific feature — constraints, sleeping bodies, continuous collision detection, raycasts — 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.