@melonjs/matter-adapter
v1.0.0
Published
melonJS physics adapter for matter-js
Downloads
33
Maintainers
Readme
@melonjs/matter-adapter

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-adaptermatter-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. 4–6).
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 2–4 at the cost of N× 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 ofother(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.categoryPick 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 | nullShoot 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/applyTorqueare now portable — they're declared onPhysicsAdapterand 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 tofixedRotation: falsein thebodyDefand checkadapter.capabilitiesif 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 motionTrigger 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
posand expects an unrotated rect), keepfixedRotation: 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-matterexample). maxVelocityis emulated. matter has no native velocity cap; the adapter clamps each body's velocity inafterUpdate.- Per-body
gravityScaleis emulated. matter 0.20 only honors the engine-levelgravity.scale; the adapter applies a counter-force inbeforeUpdatefor bodies that opt out. isGroundedis literal. It returnstruewhenever any contact pair has the other body's center below this one's. Inside anonCollisionStarthandler for a stomp, the enemy you just landed on already counts as "ground" — so don't use!isGroundedas 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 theonCollision(response, other)handler, start with the wiki's Migrating to the Physics Adapter API guide — it covers the legacy → declarativebodyDef+ 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:
- Author the slope as a 3-vertex triangle (no approach wall).
- Detect slope contact in
onCollisionActiveand 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
- Verify the boot banner shows
physic: @melonjs/matter-adapter - Set a reasonable
gravity(e.g.{ x: 0, y: 4 }for arcade platformers) - Divide your existing
applyForcemagnitudes by ~30–100 as a starting point - Use
response.normal.y < -0.7for stomp checks instead of!isGrounded(matter-native MTV; portable to builtin too) - Mark trigger / one-way / pickup bodies as
isSensor: trueto disable physical resolution while still firing the lifecycle handlers - Replace slope-response hacks with snap-to-surface in
onCollisionActive - Convert TMX polylines to thin rectangles at load time
- Pass
fixedRotation: trueinbodyDeffor 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 (
applyForcefor walking,setVelocityfor the jump impulse +applyForcefor 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
