Skip to content

Migrating to the Physics Adapter API

Olivier Biot edited this page May 15, 2026 · 3 revisions

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 legacy onCollision is 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.

Body construction

Before — legacy Body API

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;
    }
}

After — declarative bodyDef

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:

  • Body no 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.body becomes available after Container.addChild runs (typically inside onActivateEvent or the next frame).

Setting velocity and forces

Before

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);
}

After

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.

Collision handlers

Before

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;
}

After

// 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:

  • onCollisiononCollisionStart / onCollisionActive / onCollisionEnd is now the recommended lifecycle. onCollision is the legacy alias for onCollisionActive (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) replaces response.overlapV.y > 0. The pre-contact velocity signal is identical on every adapter; the response object's shape is engine-specific (SAT exposes overlapV/overlapN, matter exposes normal/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+.

API equivalence reference

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

Why bother?

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.

See also

Clone this wiki locally