npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@melonjs/matter-adapter

v1.0.0

Published

melonJS physics adapter for matter-js

Downloads

33

Readme

@melonjs/matter-adapter

melonJS Logo

License: MIT NPM Package

A matter-js physics adapter for melonJS — drops in for the built-in SAT physics and gives you matter's rigid-body simulation, rotational dynamics, constraints, sleeping bodies, continuous collision detection, and raycasting.

Per-object collision dispatch is already wired up. Every Renderable receives onCollisionStart(response, other), onCollisionActive(response, other), and onCollisionEnd(response, other) callbacks — the same shape you use under the built-in adapter. No rewrite. No world-level pair firehose to filter yourself. The matter integration handles the pair-to-renderable routing for you.

Installation

npm install @melonjs/matter-adapter

matter-js is bundled in as a regular dependency, so you don't need to install it yourself — that's the whole point of the adapter. The only peer dependency is melonjs ≥ 19.5 (because melonJS is the one providing the PhysicsAdapter interface this package implements).

Usage

Pass a MatterAdapter instance as the physic option when constructing your Application:

import { MatterAdapter } from "@melonjs/matter-adapter";
import { Application, video } from "melonjs";

const app = new Application(800, 600, {
    parent: "screen",
    renderer: video.AUTO,
    physic: new MatterAdapter({
        gravity: { x: 0, y: 5 },
    }),
});

That's it — every renderable that declares a bodyDef gets registered with matter automatically on Container.addChild, and the rest of your game code (collision handlers, velocity reads, gravity tweaks, etc.) talks to the shared PhysicsAdapter interface so it works with either adapter.

Constructor options

new MatterAdapter({
    gravity?: { x: number; y: number };        // default { x: 0, y: 1 }
    subSteps?: number;                         // default 1
    matterEngineOptions?: Matter.IEngineDefinition; // pass-through to Matter.Engine.create
})

The defaults are matter-js native (gravity = (0, 1) with gravity.scale = 0.001). For an arcade-feel platformer where the player moves a few px per step, you usually want a stronger gravity.y (e.g. 46).

subSteps divides the per-frame delta into N smaller integration steps. Matter's broad phase isn't swept — a body that moves more than ~one collision radius per tick can tunnel through a wall or another body — so for very fast objects (break shots in a pool game, projectiles, high-velocity launches) bump this to 24 at the cost of solver work. The default 1 reproduces the legacy single-step behaviour exactly.

Collision Events

The adapter dispatches matter's three native collision events to renderable hooks:

class Player extends Sprite {
    // fires once when two bodies begin contact
    onCollisionStart(response, other) { /* stomp, pickup, trigger entry */ }

    // fires every frame while two bodies remain in contact
    onCollisionActive(response, other) { /* sustained damage, conveyor friction */ }

    // fires once when two bodies separate
    onCollisionEnd(response, other) { /* left the platform, exited a zone */ }
}

Implement only the ones you need — missing methods are silently skipped. The same three handlers also fire on the builtin SAT adapter (it synthesizes start/end via a frame diff), so handler code stays portable.

The response object

The first argument passed to every collision hook is a matter-native response object:

response = {
    a:      Renderable,                 // this renderable (the one whose handler is firing)
    b:      Renderable,                 // the other renderable
    normal: { x: number, y: number },   // unit MTV for `a` (direction to escape)
    depth:  number,                     // penetration depth (always positive)
    pair:   Matter.Pair,                // raw matter pair (supports, tangent, bodies, …)
}

normal direction — the minimum-translation vector for the receiver. It points in the direction this (a.k.a. response.a) must move to separate from other. Each side of the dispatch sees its own MTV, so the normals on the two handlers are mirrored.

In canvas coordinates (y grows downward):

  • normal.y < -0.7 → push me up to escape ⇒ I'm sitting on top of other (classic stomp / landing).
  • normal.y > 0.7 → push me down to escape ⇒ I'm underneath (head-bumped a ceiling, got stomped on).
  • Math.abs(normal.x) > 0.7 → mostly horizontal contact ⇒ side hit.

response.pair is matter's native Pair (with bodyA, bodyB, collision.supports, collision.tangent, etc.) for advanced use. Note that pair.collision.normal is matter's raw normal (always the MTV of pair.bodyA); use the symmetric response.normal unless you specifically need the body-A-relative form.

Body helper methods

The canonical portable surface is the PhysicsAdapter interface — every method below is also reachable as adapter.X(renderable, ...). As a convenience this adapter bolts the same operations onto renderable.body so the idiomatic form available on built-in me.Body works here too:

// Linear kinematics
body.setVelocity(x, y)                       // ⇔ adapter.setVelocity(renderable, { x, y })
body.getVelocity(out?)                       // ⇔ adapter.getVelocity(renderable, out)
body.applyForce(x, y)                        // ⇔ adapter.applyForce(renderable, { x, y })
body.applyForce(x, y, pointX, pointY)        // off-centre force ⇒ generates torque (matter native)
body.applyImpulse(x, y)                      // ⇔ adapter.applyImpulse(renderable, { x, y })

// Angular kinematics
body.setAngle(rad)                           // ⇔ adapter.setAngle(renderable, rad)
body.getAngle()                              // ⇔ adapter.getAngle(renderable)
body.setAngularVelocity(omega)               // ⇔ adapter.setAngularVelocity(renderable, omega)
body.getAngularVelocity()                    // ⇔ adapter.getAngularVelocity(renderable)
body.applyTorque(t)                          // ⇔ adapter.applyTorque(renderable, t)

// Body state
body.setSensor(isSensor?)                    // ⇔ adapter.setSensor(renderable, isSensor)
body.setStatic(isStatic?)                    // ⇔ adapter.setStatic(renderable, isStatic)
body.setMass(m)                              // wraps Matter.Body.setMass
body.setBounce(r)                            // writes Matter.Body.restitution
body.setGravityScale(s)                      // ⇔ adapter.setGravityScale(renderable, s)
body.setCollisionMask(mask)                  // writes Matter.Body.collisionFilter.mask
body.setCollisionType(type)                  // writes Matter.Body.collisionFilter.category

Pick whichever reads better at the call site — both forms are portable. The raw matter free functions (Matter.Body.setVelocity(body, v) etc.) are not stripped, so they remain available if you want to keep matter idioms verbatim.

Reaching matter-native body fields

The fields that matter exposes on its Body interface (frictionAir, friction, restitution, angle, angularVelocity, torque, inertia, …) are reachable directly on the body. Cast to the published MatterAdapter.Body type to keep type-checking happy without taking a direct matter-js import:

import { MatterAdapter } from "@melonjs/matter-adapter";

// e.g. tune ball-felt drag based on speed (the pool-matter example uses this
// for a sliding-vs-rolling friction split):
(ball.body as MatterAdapter.Body).frictionAir = isSliding ? 0.018 : 0.003;

// read matter-native angular velocity:
const omega = (ball.body as MatterAdapter.Body).angularVelocity;

MatterAdapter.Body is ReturnType<typeof Matter.Body.create> & PhysicsBody — you get matter's full instance shape plus the portable helper methods, without needing to import matter-js yourself. Code that does this is matter-only by definition; for portable rotation use the angular kinematic methods above.

Raycasting

adapter.raycast(from: Vector2d, to: Vector2d) → RaycastHit | null

Shoot a ray through the world and get the first body hit:

const hit = adapter.raycast(new Vector2d(0, 0), new Vector2d(800, 600));
if (hit) {
    // hit.renderable — the renderable the ray entered
    // hit.point      — world-space entry point on the body's surface
    // hit.normal     — outward-facing surface normal at the entry
    // hit.fraction   — 0..1 along the ray, from `from` to `to`
}

Portable — same shape under the builtin SAT adapter, this one, and @melonjs/planck-adapter. Implementation walks each candidate body's vertices via per-edge segment intersection, so the reported point and normal reflect actual entry geometry rather than the body centre.

Note on rotation: setAngle / setAngularVelocity / applyTorque are now portable — they're declared on PhysicsAdapter and implemented by both this adapter and the builtin adapter. Under matter, rotation is fully solver-aware (the body's collision shape rotates and contact response reflects it); under builtin, rotation is visual-only (the SAT solver still tests axis-aligned shapes but the renderable's transform tracks the body's angle). Code that needs rotation-correct contact response should also opt in to fixedRotation: false in the bodyDef and check adapter.capabilities if it must branch.

Region queries

adapter.queryAABB(rect: Rect) → Renderable[]

Return every renderable whose body bounds overlap the given rectangle. Useful for area-of-effect damage, mouse / touch picking, trigger-zone sweeps, AI awareness checks. Portable — same call under builtin, matter, and planck. Under matter the implementation uses Matter.Query.region over the engine's body list.

Direct engine access

For matter-specific features that don't fit the portable PhysicsAdapter surface — constraints, compound bodies, raw Events, plugins — the adapter exposes two escape hatches so you don't have to add matter-js as a direct dependency just to reach the factories:

const adapter = app.world.adapter as MatterAdapter;

// The whole matter-js namespace. Modules are named exactly as in matter's docs
// (Matter.Constraint, Matter.Composite, Matter.Bodies, Matter.Events, ...),
// so brm.io/matter-js examples copy-paste without renaming.
adapter.matter;          // typeof Matter
adapter.matter.Constraint.create({ bodyA, bodyB, stiffness: 0.04 });

// The Matter.Engine and its world (Matter.Composite that holds everything).
adapter.engine;          // Matter.Engine
adapter.engine.world;    // Matter.World — pass to Composite.add(...) / Composite.remove(...)

A complete spring constraint:

const spring = adapter.matter.Constraint.create({
    bodyA: playerSprite.body,
    bodyB: anchorSprite.body,
    stiffness: 0.04,
    length: 80,
});
adapter.matter.Composite.add(adapter.engine.world, spring);

Any code that touches adapter.matter.* or adapter.engine.* is matter-only — it will not run on the built-in adapter or any future adapter. Use the PhysicsAdapter methods (setVelocity, applyForce, setStatic, setSensor, raycast, …) for anything that should stay portable.

Recipes

Concrete patterns for common gameplay needs. Each recipe is labelled Portable (same renderable code under any adapter), Portable via velocity (same code, just route through the body's velocity rather than the contact response), or Matter-only (uses a feature gated by adapter.capabilities).

Jump — instant upward impulse (Portable)

setVelocity is the canonical "impulse" pattern on every adapter. Direct mutation of vel.y works under the builtin adapter but not under matter (matter's Verlet integrator needs both velocity and positionPrev reset together); the body method handles that for you.

const vel = this.body.getVelocity();
this.body.setVelocity(vel.x, -JUMP_VEL);  // preserves horizontal motion

Trigger zone / coin pickup (Portable)

Mark the body as a sensor — collisions still fire onCollisionStart but the solver doesn't physically push the player away. Collectable and Trigger already declare isSensor: true in their default bodyDef.

class Coin extends Sprite {
    constructor(x, y) {
        super(x, y, { image: "coin" });
        this.bodyDef = {
            type: "static",
            shapes: [new Ellipse(16, 16, 32, 32)],
            isSensor: true,
            collisionType: collision.types.COLLECTABLE_OBJECT,
            collisionMask: collision.types.PLAYER_OBJECT,
        };
    }
    onCollisionStart(_response, _other) {
        gameState.score += 100;
        this.ancestor.removeChild(this);
    }
}

One-way platform (Portable)

A sensor body + manual snap-to-top from the player. Falling players land; jumping players pass through; pressing down drops through.

// Platform definition
this.bodyDef = {
    type: "static",
    shapes: [new Rect(0, 0, width, height)],
    isSensor: true,        // <-- key: matter doesn't try to resolve the contact
    collisionType: collision.types.WORLD_SHAPE,
    collisionMask: collision.types.PLAYER_OBJECT,
};

// Player handler — same on both adapters
onCollisionActive(_response, other) {
    if (other.type !== "platform") return;
    if (input.keyStatus("down")) return;              // drop-through
    const vel = this.body.getVelocity();
    if (vel.y < 0) return;                            // jumping up — pass through
    const playerBottom = this.pos.y + this.height;
    const platformTop = other.pos.y;
    if (playerBottom - platformTop > this.height * 0.5) return; // came from below
    const adapter = this.parentApp.world.adapter;
    adapter.setPosition(this, scratchPos.set(this.pos.x, platformTop - this.height));
    this.body.setVelocity(vel.x, 0);
}

Stomp detection (Portable via velocity)

Read the body's pre-contact velocity in onCollisionStart. The signal is identical on every adapter and survives mid-tick mutations that contact normals can't.

onCollisionStart(_response, other) {
    if (other.body.collisionType !== collision.types.ENEMY_OBJECT) return;
    const vel = this.body.getVelocity();
    if (vel.y > 0) {
        // I was falling at the moment of impact — stomp
        other.die();
        this.body.setVelocity(vel.x, -STOMP_BOUNCE);
    } else {
        this.hurt();
    }
}

If you do want adapter-native contact info (slope normals, penetration depth) you can branch on adapter.name === "@melonjs/matter-adapter" and reach response.normal / response.depth / response.pair — but that handler is no longer portable.

Spring / hinge between two bodies (Matter-only)

Matter constraints are reached via the adapter's matter escape hatch — no need to add matter-js as a direct dependency.

const adapter = app.world.adapter as MatterAdapter;
if (adapter.capabilities.constraints) {
    const spring = adapter.matter.Constraint.create({
        bodyA: anchor.body,
        bodyB: player.body,
        stiffness: 0.04,    // 0 = floppy rope, 1 = rigid rod
        length: 80,
    });
    adapter.matter.Composite.add(adapter.engine.world, spring);
}

Use stiffness: 1 for a rigid hinge, low stiffness (~0.01–0.05) for spring-like behaviour. Set pointA / pointB to attach the constraint at a point offset from each body's centre.

Sleeping bodies (Matter-only)

Matter can mark idle bodies as "sleeping" and skip integrating them entirely until disturbed — a meaningful CPU win when you have dozens of static-after-settling props (debris, fallen blocks, settled stacks). Enable at the engine level via the constructor, then leave matter to manage sleep state.

new MatterAdapter({
    gravity: { x: 0, y: 5 },
    matterEngineOptions: {
        enableSleeping: true,
    },
});

// Wake a specific body programmatically if needed (e.g. on a trigger event):
const adapter = app.world.adapter as MatterAdapter;
if (adapter.capabilities.sleepingBodies) {
    adapter.matter.Sleeping.set(this.body, false);
}

The builtin adapter has no equivalent — there's no integration cost to skip, since SAT only runs collisions, not Verlet integration.

Body Definitions

melonJS body definitions (BodyDefinition) are mapped to matter bodies. The keys you can set are the same as for the builtin adapter:

this.bodyDef = {
    type: "dynamic" | "static",
    shapes: BodyShape[],         // Rect, Polygon, Ellipse, etc.
    collisionType?: number,
    collisionMask?: number,
    maxVelocity?: { x, y },
    frictionAir?: number | { x, y },
    restitution?: number,
    density?: number,
    gravityScale?: number,
    isSensor?: boolean,
    fixedRotation?: boolean,     // matter only — defaults to true
};

Collision filter API

For matter users, the matter-native body.collisionFilter.category / mask is exposed as a live alias of the legacy body.collisionType / collisionMask:

// All four lines do the same thing — pick whichever convention you prefer:
body.collisionFilter.category = collision.types.PLAYER_OBJECT;
body.collisionType = collision.types.PLAYER_OBJECT;

body.collisionFilter.mask = collision.types.ENEMY_OBJECT;
body.collisionMask = collision.types.ENEMY_OBJECT;

Behavioural notes when porting from the builtin adapter

  • Bodies have full rotational dynamics by default for non-fixedRotation bodies. If your game code assumes axis-aligned bodies (e.g. it reads pos and expects an unrotated rect), keep fixedRotation: true (the default).
  • Polylines (zero-thickness lines) don't translate. matter can't make a body from collinear vertices. Give them a small thickness, or load the TMX shape and rewrite it post-load (see the platformer-matter example).
  • maxVelocity is emulated. matter has no native velocity cap; the adapter clamps each body's velocity in afterUpdate.
  • Per-body gravityScale is emulated. matter 0.20 only honors the engine-level gravity.scale; the adapter applies a counter-force in beforeUpdate for bodies that opt out.
  • isGrounded is literal. It returns true whenever any contact pair has the other body's center below this one's. Inside an onCollisionStart handler for a stomp, the enemy you just landed on already counts as "ground" — so don't use !isGrounded as a proxy for "I was airborne before this contact."

Porting from the built-in adapter

The PhysicsAdapter interface is the portable surface. If your gameplay code only uses methods on world.adapter (and the four collision hooks on Renderable), most of it ports without changes. The pitfalls below cover the parts that don't.

Coming from a pre-19.5 codebase? If your game still uses the legacy new me.Body(this, shape) pattern and the onCollision(response, other) handler, start with the wiki's Migrating to the Physics Adapter API guide — it covers the legacy → declarative bodyDef + lifecycle-handler migration on the built-in adapter. The notes below pick up after that, for the actual built-in → matter swap. See also Switching Physics Adapters and BuiltinAdapter Quirks for engine-portability gotchas.

isGrounded is literal, not predictive

It returns true whenever there's an active contact with a body whose center is below ours. Inside an onCollisionStart for a stomp, the enemy you just landed on already counts as "ground" — so !isGrounded is not a reliable "I was airborne before this contact" check. Use the body's pre-contact velocity instead (vel.y > 0 = falling at impact).

Matter forces are much smaller than builtin forces

applyForce on matter integrates as force / mass * dt². With a typical 64×96 sprite (mass ≈ 6) and dt ≈ 16, a force of 0.05 already moves the body noticeably. If you ported a builtin WALK_FORCE = 0.4 directly, the player rockets across the screen. Start ~100× smaller and tune up.

applyForce is not a one-shot impulse

It's a sustained Newtonian force, reset at the end of each step. The single-step contribution depends on dt and mass, so it's a fragile way to do "instant velocity change" — for jumps, dashes, knockbacks, use setVelocity (immediate) or applyImpulse.

Sensor bodies disable physical resolution

To make a body non-solid (one-way platform, trigger zone, ground-snap assist), mark it as a sensor:

adapter.setSensor(platform, true);
// or declaratively on the body def:
bodyDef.isSensor = true;

A sensor still fires onCollisionStart / onCollisionActive / onCollisionEnd — only the physical separation is disabled.

Slopes need to be authored as proper ramps OR use snap-to-surface

The builtin adapter's slope handling pattern (mutating response.overlapV.y to force the player up regardless of contact angle) doesn't translate — matter resolves contacts based on actual geometry. If your TMX has a slope polygon with a vertical "approach wall," matter will block the player. Two ways out:

  1. Author the slope as a 3-vertex triangle (no approach wall).
  2. Detect slope contact in onCollisionActive and manually snap the player to the slope's surface Y at their X (adapter.setPosition(player, x, surfaceY - playerHeight)). The matter-platformer example uses this pattern.

One-way platforms

A sensor body + manual landing snap. Falling players land; jumping players pass through; pressing down drops through:

// at load time
adapter.setSensor(platform, true);

// in player.onCollisionActive(_response, other):
if (other.type === "platform") {
    if (input.keyStatus("down")) return;           // drop through
    const vel = adapter.getVelocity(this);
    if (vel.y < 0) return;                          // jumping up
    const playerBottom = this.pos.y + this.height;
    if (playerBottom > other.pos.y + 16) return;    // too deep — came from below
    adapter.setPosition(this, new Vector2d(this.pos.x, other.pos.y - this.height));
    adapter.setVelocity(this, new Vector2d(vel.x, 0));
}

TMX polylines

matter can't build a body from collinear vertices. Replace polyline bodies with thin rectangles at load time (adapter.updateShape(obj, [new Rect(0, 0, width, 6)])).

body.position vs renderable.pos

matter's body.position is the centroid; renderable.pos is top-left (with anchor 0,0). The adapter keeps the two in sync via a stored offset — your gameplay code should read renderable.pos for visual placement; only reach for body.position for matter-internal needs.

A simple porting example

A minimal player entity. The same class on the built-in adapter, then ported here. Numbered comments call out what changed and why.

Before — built-in (SAT) adapter:

import { Application, collision, input, Rect, Sprite, video } from "melonjs";

new Application(800, 600, {
    parent: "screen",
    renderer: video.AUTO,
    // physic: defaults to BuiltinAdapter
});

const MAX_VEL_X = 3;
const MAX_VEL_Y = 15;

class Player extends Sprite {
    constructor(x: number, y: number) {
        super(x, y, { image: "player", framewidth: 64, frameheight: 96 });

        this.bodyDef = {
            type: "dynamic",
            shapes: [new Rect(0, 0, 64, 96)],
            collisionType: collision.types.PLAYER_OBJECT,
            maxVelocity: { x: MAX_VEL_X, y: MAX_VEL_Y },
            frictionAir: { x: 0.4, y: 0 },
        };

        input.bindKey(input.KEY.LEFT,  "left");
        input.bindKey(input.KEY.RIGHT, "right");
        input.bindKey(input.KEY.UP,    "jump", true);
    }

    update(dt: number) {
        const adapter = this.parentApp.world.adapter;
        const vel = adapter.getVelocity(this);

        if (input.isKeyPressed("left")) {
            adapter.applyForce(this, { x: -MAX_VEL_X, y: 0 });
        } else if (input.isKeyPressed("right")) {
            adapter.applyForce(this, { x:  MAX_VEL_X, y: 0 });
        }
        if (input.isKeyPressed("jump")) {
            adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y });
        }

        return super.update(dt);
    }

    onCollisionStart(response, other) {
        if (other.body.collisionType !== collision.types.ENEMY_OBJECT) return;
        const adapter = this.parentApp.world.adapter;
        if (response.normal.y < -0.7) {
            // I'm on top of the enemy — stomp
            const vel = adapter.getVelocity(this);
            adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y * 0.75 });
            return;
        }
        this.hurt();
    }
}

After — @melonjs/matter-adapter:

import { Application, collision, input, Rect, Sprite, video } from "melonjs";
import { MatterAdapter } from "@melonjs/matter-adapter";

new Application(800, 600, {
    parent: "screen",
    renderer: video.AUTO,
    // (1) Swap the adapter. Pick a gravity that suits your sprite scale.
    physic: new MatterAdapter({ gravity: { x: 0, y: 5 } }),
});

const MAX_VEL_X = 3;
const MAX_VEL_Y = 15;
// (2) Matter forces are Newtonian (force/mass*dt²) — magnitudes ~100× smaller.
const WALK_FORCE = 0.012;

class Player extends Sprite {
    constructor(x: number, y: number) {
        super(x, y, { image: "player", framewidth: 64, frameheight: 96 });

        // bodyDef is portable — shape, collision type, maxVelocity unchanged.
        // (3) frictionAir is scalar in matter (no per-axis variant).
        this.bodyDef = {
            type: "dynamic",
            shapes: [new Rect(0, 0, 64, 96)],
            collisionType: collision.types.PLAYER_OBJECT,
            maxVelocity: { x: MAX_VEL_X, y: MAX_VEL_Y },
            frictionAir: 0.02,
        };

        input.bindKey(input.KEY.LEFT,  "left");
        input.bindKey(input.KEY.RIGHT, "right");
        input.bindKey(input.KEY.UP,    "jump", true);
    }

    update(dt: number) {
        const adapter = this.parentApp.world.adapter;
        const vel = adapter.getVelocity(this);

        // (4) Same applyForce calls — only the magnitude changed.
        if (input.isKeyPressed("left")) {
            adapter.applyForce(this, { x: -WALK_FORCE, y: 0 });
        } else if (input.isKeyPressed("right")) {
            adapter.applyForce(this, { x:  WALK_FORCE, y: 0 });
        }
        // (5) setVelocity is portable — works the same way on both adapters.
        if (input.isKeyPressed("jump")) {
            adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y });
        }

        return super.update(dt);
    }

    // (6) Stomp logic is unchanged — `response.normal` and `onCollisionStart`
    //     are part of the portable API and behave identically on both adapters.
    onCollisionStart(response, other) {
        if (other.body.collisionType !== collision.types.ENEMY_OBJECT) return;
        if (response.normal.y < -0.7) {
            const adapter = this.parentApp.world.adapter;
            const vel = adapter.getVelocity(this);
            adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y * 0.75 });
            return;
        }
        this.hurt();
    }
}

Summary of changes:

| # | Change | Reason | |---|---|---| | 1 | Pass MatterAdapter to physic | switching engines | | 2 | WALK_FORCE = 0.012 (down from 3) | Newtonian magnitudes are much smaller | | 3 | frictionAir: 0.02 (scalar) | matter has no per-axis air friction | | 4 | applyForce calls unchanged | portable API | | 5 | setVelocity for jump unchanged | portable; correct pattern for instant velocity change | | 6 | onCollisionStart + response.normal unchanged | portable lifecycle handler with identical contract on both adapters |

bodyDef shape, collision masks, max-velocity cap, key bindings, sprite setup, collision handler — all unchanged.

Default behaviour differences vs builtin

| Behaviour | Builtin | Matter | |---|---|---| | fixedRotation default | n/a (SAT bodies don't rotate) | true — matches SAT. Set false in bodyDef to enable rotation. | | Continuous collision detection | ❌ | ✅ | | Sleeping bodies | ❌ | ✅ | | Constraints (springs, joints) | ❌ | ✅ via Matter.Constraint (reach via adapter.engine) | | applyForce units | px/frame² | matter Newtonian |

Porting checklist

  1. Verify the boot banner shows physic: @melonjs/matter-adapter
  2. Set a reasonable gravity (e.g. { x: 0, y: 4 } for arcade platformers)
  3. Divide your existing applyForce magnitudes by ~30–100 as a starting point
  4. Use response.normal.y < -0.7 for stomp checks instead of !isGrounded (matter-native MTV; portable to builtin too)
  5. Mark trigger / one-way / pickup bodies as isSensor: true to disable physical resolution while still firing the lifecycle handlers
  6. Replace slope-response hacks with snap-to-surface in onCollisionActive
  7. Convert TMX polylines to thin rectangles at load time
  8. Pass fixedRotation: true in bodyDef for anything that should stay axis-aligned

Examples

The melonJS repo's platformer-matter example is a full port of the canonical platformer running on this adapter, including:

  • Matter-native player movement (applyForce for walking, setVelocity for the jump impulse + applyForce for the variable-height hold)
  • Velocity-based stomp detection
  • Slope-grip via onCollisionActive
  • One-way platforms via setSensor + manual landing snap
  • A live physic: line in the boot banner showing this adapter was loaded

License

MIT