@melonjs/matter-adapter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +618 -0
- package/build/index.d.ts +208 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +1173 -0
- package/build/index.js.map +7 -0
- package/package.json +74 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 - _2026-05-22_
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`MatterAdapter.Body`** — published type for `renderable.body` under this adapter. Namespace-merged with the class, defined as `ReturnType<typeof Matter.Body.create> & PhysicsBody`. Lets user code reach matter-native fields (`frictionAir`, `angle`, `angularVelocity`, `torque`, …) via `(this.body as MatterAdapter.Body).frictionAir = 0.02` without importing `matter-js` directly — the matter dependency stays behind the adapter boundary.
|
|
7
|
+
- **`subSteps` option** on `MatterAdapterOptions` (default 1). Runs `Matter.Engine.update(engine, dt / N)` N times per `step()` call. Increases narrow-phase accuracy at high relative velocities (break shots, projectiles) at the cost of ~N× physics CPU. Matter's broad phase isn't swept, so a body moving more than ~one collision radius per tick can tunnel through a wall or other body; smaller per-tick deltas cap inter-body motion and eliminate the tunneling.
|
|
8
|
+
- **Angular API helpers spliced onto each body**: `setAngularVelocity(omega)` / `getAngularVelocity()` / `setAngle(rad)` / `getAngle()` / `applyTorque(t)`. Match the corresponding `PhysicsBody` interface methods. `applyForce(x, y)` extended to accept an optional point `(x, y, pointX, pointY)` — when present, forwarded to `Matter.Body.applyForce(body, point, F)` so matter's native lever-arm handling generates the corresponding torque.
|
|
9
|
+
- **Adapter-level angular methods**: `setAngle` / `getAngle` / `setAngularVelocity` / `getAngularVelocity` / `applyTorque` / `applyForce(rend, F, point?)`. Mirror the body-level helpers for portable code.
|
|
10
|
+
- **`BodyDefinition.friction` passthrough** — matter's `body.friction` (surface coefficient of friction) is now honored on body registration. Combined with `fixedRotation: false`, produces the matter-native "throw" effect between balls and rail-friction-induced trajectory changes off walls.
|
|
11
|
+
- **Helpers contract enforced by the type system** — the `helpers` object spliced onto each body at registration is typed `Omit<PhysicsBody, "collisionType" | "collisionMask">`. Drift between the matter helpers and the engine's portable `PhysicsBody` interface now fails the matter-adapter build immediately.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **`syncFromPhysics` rotation pivot** — the per-frame transform sync set `currentTransform.identity().rotate(body.angle)`, which `Renderable.preDraw` then applies pivoted at `renderable.pos`. For renderables with `anchorPoint = (0, 0)` and a body shape centered inside the bounds (the common case), the sprite rotated around its top-left corner instead of its visible center. Now pre-translates by the negated `posOffset` (the centroid → pos delta cached at addBody time), so rotation lands on the visible center regardless of anchor.
|
|
15
|
+
|
|
16
|
+
### Notes
|
|
17
|
+
- Initial release.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2011 - 2026 Olivier Biot (AltByte Pte Ltd)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
# @melonjs/matter-adapter
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](https://github.com/melonjs/melonJS/blob/master/LICENSE)
|
|
6
|
+
[](https://www.npmjs.com/package/@melonjs/matter-adapter)
|
|
7
|
+
|
|
8
|
+
A [matter-js](https://brm.io/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.
|
|
9
|
+
|
|
10
|
+
**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.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @melonjs/matter-adapter
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`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).
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Pass a `MatterAdapter` instance as the `physic` option when constructing your `Application`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { MatterAdapter } from "@melonjs/matter-adapter";
|
|
26
|
+
import { Application, video } from "melonjs";
|
|
27
|
+
|
|
28
|
+
const app = new Application(800, 600, {
|
|
29
|
+
parent: "screen",
|
|
30
|
+
renderer: video.AUTO,
|
|
31
|
+
physic: new MatterAdapter({
|
|
32
|
+
gravity: { x: 0, y: 5 },
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
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`](https://melonjs.github.io/melonJS/) interface so it works with either adapter.
|
|
38
|
+
|
|
39
|
+
### Constructor options
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
new MatterAdapter({
|
|
43
|
+
gravity?: { x: number; y: number }; // default { x: 0, y: 1 }
|
|
44
|
+
subSteps?: number; // default 1
|
|
45
|
+
matterEngineOptions?: Matter.IEngineDefinition; // pass-through to Matter.Engine.create
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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`).
|
|
50
|
+
|
|
51
|
+
`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.
|
|
52
|
+
|
|
53
|
+
## Collision Events
|
|
54
|
+
|
|
55
|
+
The adapter dispatches matter's three native collision events to renderable hooks:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
class Player extends Sprite {
|
|
59
|
+
// fires once when two bodies begin contact
|
|
60
|
+
onCollisionStart(response, other) { /* stomp, pickup, trigger entry */ }
|
|
61
|
+
|
|
62
|
+
// fires every frame while two bodies remain in contact
|
|
63
|
+
onCollisionActive(response, other) { /* sustained damage, conveyor friction */ }
|
|
64
|
+
|
|
65
|
+
// fires once when two bodies separate
|
|
66
|
+
onCollisionEnd(response, other) { /* left the platform, exited a zone */ }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
### The `response` object
|
|
73
|
+
|
|
74
|
+
The first argument passed to every collision hook is a matter-native response object:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
response = {
|
|
78
|
+
a: Renderable, // this renderable (the one whose handler is firing)
|
|
79
|
+
b: Renderable, // the other renderable
|
|
80
|
+
normal: { x: number, y: number }, // unit MTV for `a` (direction to escape)
|
|
81
|
+
depth: number, // penetration depth (always positive)
|
|
82
|
+
pair: Matter.Pair, // raw matter pair (supports, tangent, bodies, …)
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**`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.
|
|
87
|
+
|
|
88
|
+
In canvas coordinates (y grows downward):
|
|
89
|
+
|
|
90
|
+
- `normal.y < -0.7` → push me **up** to escape ⇒ I'm sitting on top of `other` (classic stomp / landing).
|
|
91
|
+
- `normal.y > 0.7` → push me **down** to escape ⇒ I'm underneath (head-bumped a ceiling, got stomped on).
|
|
92
|
+
- `Math.abs(normal.x) > 0.7` → mostly horizontal contact ⇒ side hit.
|
|
93
|
+
|
|
94
|
+
`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.
|
|
95
|
+
|
|
96
|
+
## Body helper methods
|
|
97
|
+
|
|
98
|
+
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:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// Linear kinematics
|
|
102
|
+
body.setVelocity(x, y) // ⇔ adapter.setVelocity(renderable, { x, y })
|
|
103
|
+
body.getVelocity(out?) // ⇔ adapter.getVelocity(renderable, out)
|
|
104
|
+
body.applyForce(x, y) // ⇔ adapter.applyForce(renderable, { x, y })
|
|
105
|
+
body.applyForce(x, y, pointX, pointY) // off-centre force ⇒ generates torque (matter native)
|
|
106
|
+
body.applyImpulse(x, y) // ⇔ adapter.applyImpulse(renderable, { x, y })
|
|
107
|
+
|
|
108
|
+
// Angular kinematics
|
|
109
|
+
body.setAngle(rad) // ⇔ adapter.setAngle(renderable, rad)
|
|
110
|
+
body.getAngle() // ⇔ adapter.getAngle(renderable)
|
|
111
|
+
body.setAngularVelocity(omega) // ⇔ adapter.setAngularVelocity(renderable, omega)
|
|
112
|
+
body.getAngularVelocity() // ⇔ adapter.getAngularVelocity(renderable)
|
|
113
|
+
body.applyTorque(t) // ⇔ adapter.applyTorque(renderable, t)
|
|
114
|
+
|
|
115
|
+
// Body state
|
|
116
|
+
body.setSensor(isSensor?) // ⇔ adapter.setSensor(renderable, isSensor)
|
|
117
|
+
body.setStatic(isStatic?) // ⇔ adapter.setStatic(renderable, isStatic)
|
|
118
|
+
body.setMass(m) // wraps Matter.Body.setMass
|
|
119
|
+
body.setBounce(r) // writes Matter.Body.restitution
|
|
120
|
+
body.setGravityScale(s) // ⇔ adapter.setGravityScale(renderable, s)
|
|
121
|
+
body.setCollisionMask(mask) // writes Matter.Body.collisionFilter.mask
|
|
122
|
+
body.setCollisionType(type) // writes Matter.Body.collisionFilter.category
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
### Reaching matter-native body fields
|
|
128
|
+
|
|
129
|
+
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:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { MatterAdapter } from "@melonjs/matter-adapter";
|
|
133
|
+
|
|
134
|
+
// e.g. tune ball-felt drag based on speed (the pool-matter example uses this
|
|
135
|
+
// for a sliding-vs-rolling friction split):
|
|
136
|
+
(ball.body as MatterAdapter.Body).frictionAir = isSliding ? 0.018 : 0.003;
|
|
137
|
+
|
|
138
|
+
// read matter-native angular velocity:
|
|
139
|
+
const omega = (ball.body as MatterAdapter.Body).angularVelocity;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`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.
|
|
143
|
+
|
|
144
|
+
## Raycasting
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
adapter.raycast(from: Vector2d, to: Vector2d) → RaycastHit | null
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Shoot a ray through the world and get the first body hit:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const hit = adapter.raycast(new Vector2d(0, 0), new Vector2d(800, 600));
|
|
154
|
+
if (hit) {
|
|
155
|
+
// hit.renderable — the renderable the ray entered
|
|
156
|
+
// hit.point — world-space entry point on the body's surface
|
|
157
|
+
// hit.normal — outward-facing surface normal at the entry
|
|
158
|
+
// hit.fraction — 0..1 along the ray, from `from` to `to`
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
> **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.
|
|
165
|
+
|
|
166
|
+
## Region queries
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
adapter.queryAABB(rect: Rect) → Renderable[]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
174
|
+
## Direct engine access
|
|
175
|
+
|
|
176
|
+
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:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const adapter = app.world.adapter as MatterAdapter;
|
|
180
|
+
|
|
181
|
+
// The whole matter-js namespace. Modules are named exactly as in matter's docs
|
|
182
|
+
// (Matter.Constraint, Matter.Composite, Matter.Bodies, Matter.Events, ...),
|
|
183
|
+
// so brm.io/matter-js examples copy-paste without renaming.
|
|
184
|
+
adapter.matter; // typeof Matter
|
|
185
|
+
adapter.matter.Constraint.create({ bodyA, bodyB, stiffness: 0.04 });
|
|
186
|
+
|
|
187
|
+
// The Matter.Engine and its world (Matter.Composite that holds everything).
|
|
188
|
+
adapter.engine; // Matter.Engine
|
|
189
|
+
adapter.engine.world; // Matter.World — pass to Composite.add(...) / Composite.remove(...)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A complete spring constraint:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const spring = adapter.matter.Constraint.create({
|
|
196
|
+
bodyA: playerSprite.body,
|
|
197
|
+
bodyB: anchorSprite.body,
|
|
198
|
+
stiffness: 0.04,
|
|
199
|
+
length: 80,
|
|
200
|
+
});
|
|
201
|
+
adapter.matter.Composite.add(adapter.engine.world, spring);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
206
|
+
## Recipes
|
|
207
|
+
|
|
208
|
+
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`).
|
|
209
|
+
|
|
210
|
+
### Jump — instant upward impulse (Portable)
|
|
211
|
+
|
|
212
|
+
`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.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const vel = this.body.getVelocity();
|
|
216
|
+
this.body.setVelocity(vel.x, -JUMP_VEL); // preserves horizontal motion
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Trigger zone / coin pickup (Portable)
|
|
220
|
+
|
|
221
|
+
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`.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
class Coin extends Sprite {
|
|
225
|
+
constructor(x, y) {
|
|
226
|
+
super(x, y, { image: "coin" });
|
|
227
|
+
this.bodyDef = {
|
|
228
|
+
type: "static",
|
|
229
|
+
shapes: [new Ellipse(16, 16, 32, 32)],
|
|
230
|
+
isSensor: true,
|
|
231
|
+
collisionType: collision.types.COLLECTABLE_OBJECT,
|
|
232
|
+
collisionMask: collision.types.PLAYER_OBJECT,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
onCollisionStart(_response, _other) {
|
|
236
|
+
gameState.score += 100;
|
|
237
|
+
this.ancestor.removeChild(this);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### One-way platform (Portable)
|
|
243
|
+
|
|
244
|
+
A sensor body + manual snap-to-top from the player. Falling players land; jumping players pass through; pressing _down_ drops through.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// Platform definition
|
|
248
|
+
this.bodyDef = {
|
|
249
|
+
type: "static",
|
|
250
|
+
shapes: [new Rect(0, 0, width, height)],
|
|
251
|
+
isSensor: true, // <-- key: matter doesn't try to resolve the contact
|
|
252
|
+
collisionType: collision.types.WORLD_SHAPE,
|
|
253
|
+
collisionMask: collision.types.PLAYER_OBJECT,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Player handler — same on both adapters
|
|
257
|
+
onCollisionActive(_response, other) {
|
|
258
|
+
if (other.type !== "platform") return;
|
|
259
|
+
if (input.keyStatus("down")) return; // drop-through
|
|
260
|
+
const vel = this.body.getVelocity();
|
|
261
|
+
if (vel.y < 0) return; // jumping up — pass through
|
|
262
|
+
const playerBottom = this.pos.y + this.height;
|
|
263
|
+
const platformTop = other.pos.y;
|
|
264
|
+
if (playerBottom - platformTop > this.height * 0.5) return; // came from below
|
|
265
|
+
const adapter = this.parentApp.world.adapter;
|
|
266
|
+
adapter.setPosition(this, scratchPos.set(this.pos.x, platformTop - this.height));
|
|
267
|
+
this.body.setVelocity(vel.x, 0);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Stomp detection (Portable via velocity)
|
|
272
|
+
|
|
273
|
+
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.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
onCollisionStart(_response, other) {
|
|
277
|
+
if (other.body.collisionType !== collision.types.ENEMY_OBJECT) return;
|
|
278
|
+
const vel = this.body.getVelocity();
|
|
279
|
+
if (vel.y > 0) {
|
|
280
|
+
// I was falling at the moment of impact — stomp
|
|
281
|
+
other.die();
|
|
282
|
+
this.body.setVelocity(vel.x, -STOMP_BOUNCE);
|
|
283
|
+
} else {
|
|
284
|
+
this.hurt();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
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.
|
|
290
|
+
|
|
291
|
+
### Spring / hinge between two bodies (Matter-only)
|
|
292
|
+
|
|
293
|
+
Matter constraints are reached via the adapter's `matter` escape hatch — no need to add `matter-js` as a direct dependency.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
const adapter = app.world.adapter as MatterAdapter;
|
|
297
|
+
if (adapter.capabilities.constraints) {
|
|
298
|
+
const spring = adapter.matter.Constraint.create({
|
|
299
|
+
bodyA: anchor.body,
|
|
300
|
+
bodyB: player.body,
|
|
301
|
+
stiffness: 0.04, // 0 = floppy rope, 1 = rigid rod
|
|
302
|
+
length: 80,
|
|
303
|
+
});
|
|
304
|
+
adapter.matter.Composite.add(adapter.engine.world, spring);
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
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.
|
|
309
|
+
|
|
310
|
+
### Sleeping bodies (Matter-only)
|
|
311
|
+
|
|
312
|
+
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.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
new MatterAdapter({
|
|
316
|
+
gravity: { x: 0, y: 5 },
|
|
317
|
+
matterEngineOptions: {
|
|
318
|
+
enableSleeping: true,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Wake a specific body programmatically if needed (e.g. on a trigger event):
|
|
323
|
+
const adapter = app.world.adapter as MatterAdapter;
|
|
324
|
+
if (adapter.capabilities.sleepingBodies) {
|
|
325
|
+
adapter.matter.Sleeping.set(this.body, false);
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The builtin adapter has no equivalent — there's no integration cost to skip, since SAT only runs collisions, not Verlet integration.
|
|
330
|
+
|
|
331
|
+
## Body Definitions
|
|
332
|
+
|
|
333
|
+
melonJS body definitions (`BodyDefinition`) are mapped to matter bodies. The keys you can set are the same as for the builtin adapter:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
this.bodyDef = {
|
|
337
|
+
type: "dynamic" | "static",
|
|
338
|
+
shapes: BodyShape[], // Rect, Polygon, Ellipse, etc.
|
|
339
|
+
collisionType?: number,
|
|
340
|
+
collisionMask?: number,
|
|
341
|
+
maxVelocity?: { x, y },
|
|
342
|
+
frictionAir?: number | { x, y },
|
|
343
|
+
restitution?: number,
|
|
344
|
+
density?: number,
|
|
345
|
+
gravityScale?: number,
|
|
346
|
+
isSensor?: boolean,
|
|
347
|
+
fixedRotation?: boolean, // matter only — defaults to true
|
|
348
|
+
};
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Collision filter API
|
|
352
|
+
|
|
353
|
+
For matter users, the matter-native `body.collisionFilter.category` / `mask` is exposed as a live alias of the legacy `body.collisionType` / `collisionMask`:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// All four lines do the same thing — pick whichever convention you prefer:
|
|
357
|
+
body.collisionFilter.category = collision.types.PLAYER_OBJECT;
|
|
358
|
+
body.collisionType = collision.types.PLAYER_OBJECT;
|
|
359
|
+
|
|
360
|
+
body.collisionFilter.mask = collision.types.ENEMY_OBJECT;
|
|
361
|
+
body.collisionMask = collision.types.ENEMY_OBJECT;
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Behavioural notes when porting from the builtin adapter
|
|
365
|
+
|
|
366
|
+
- **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).
|
|
367
|
+
- **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).
|
|
368
|
+
- **`maxVelocity` is emulated.** matter has no native velocity cap; the adapter clamps each body's velocity in `afterUpdate`.
|
|
369
|
+
- **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.
|
|
370
|
+
- **`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."
|
|
371
|
+
|
|
372
|
+
## Porting from the built-in adapter
|
|
373
|
+
|
|
374
|
+
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.
|
|
375
|
+
|
|
376
|
+
> **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](https://github.com/melonjs/melonJS/wiki/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](https://github.com/melonjs/melonJS/wiki/Switching-Physics-Adapters) and [BuiltinAdapter Quirks](https://github.com/melonjs/melonJS/wiki/BuiltinAdapter-Quirks) for engine-portability gotchas.
|
|
377
|
+
|
|
378
|
+
### `isGrounded` is literal, not predictive
|
|
379
|
+
|
|
380
|
+
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).
|
|
381
|
+
|
|
382
|
+
### Matter forces are *much* smaller than builtin forces
|
|
383
|
+
|
|
384
|
+
`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.
|
|
385
|
+
|
|
386
|
+
### `applyForce` is not a one-shot impulse
|
|
387
|
+
|
|
388
|
+
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`.
|
|
389
|
+
|
|
390
|
+
### Sensor bodies disable physical resolution
|
|
391
|
+
|
|
392
|
+
To make a body non-solid (one-way platform, trigger zone, ground-snap assist), mark it as a sensor:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
adapter.setSensor(platform, true);
|
|
396
|
+
// or declaratively on the body def:
|
|
397
|
+
bodyDef.isSensor = true;
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
A sensor still fires `onCollisionStart` / `onCollisionActive` / `onCollisionEnd` — only the physical separation is disabled.
|
|
401
|
+
|
|
402
|
+
### Slopes need to be authored as proper ramps OR use snap-to-surface
|
|
403
|
+
|
|
404
|
+
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:
|
|
405
|
+
|
|
406
|
+
1. Author the slope as a 3-vertex triangle (no approach wall).
|
|
407
|
+
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.
|
|
408
|
+
|
|
409
|
+
### One-way platforms
|
|
410
|
+
|
|
411
|
+
A sensor body + manual landing snap. Falling players land; jumping players pass through; pressing _down_ drops through:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
// at load time
|
|
415
|
+
adapter.setSensor(platform, true);
|
|
416
|
+
|
|
417
|
+
// in player.onCollisionActive(_response, other):
|
|
418
|
+
if (other.type === "platform") {
|
|
419
|
+
if (input.keyStatus("down")) return; // drop through
|
|
420
|
+
const vel = adapter.getVelocity(this);
|
|
421
|
+
if (vel.y < 0) return; // jumping up
|
|
422
|
+
const playerBottom = this.pos.y + this.height;
|
|
423
|
+
if (playerBottom > other.pos.y + 16) return; // too deep — came from below
|
|
424
|
+
adapter.setPosition(this, new Vector2d(this.pos.x, other.pos.y - this.height));
|
|
425
|
+
adapter.setVelocity(this, new Vector2d(vel.x, 0));
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### TMX polylines
|
|
430
|
+
|
|
431
|
+
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)])`).
|
|
432
|
+
|
|
433
|
+
### `body.position` vs `renderable.pos`
|
|
434
|
+
|
|
435
|
+
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.
|
|
436
|
+
|
|
437
|
+
### A simple porting example
|
|
438
|
+
|
|
439
|
+
A minimal player entity. The same class on the built-in adapter, then ported here. Numbered comments call out what changed and why.
|
|
440
|
+
|
|
441
|
+
**Before — built-in (SAT) adapter:**
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { Application, collision, input, Rect, Sprite, video } from "melonjs";
|
|
445
|
+
|
|
446
|
+
new Application(800, 600, {
|
|
447
|
+
parent: "screen",
|
|
448
|
+
renderer: video.AUTO,
|
|
449
|
+
// physic: defaults to BuiltinAdapter
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const MAX_VEL_X = 3;
|
|
453
|
+
const MAX_VEL_Y = 15;
|
|
454
|
+
|
|
455
|
+
class Player extends Sprite {
|
|
456
|
+
constructor(x: number, y: number) {
|
|
457
|
+
super(x, y, { image: "player", framewidth: 64, frameheight: 96 });
|
|
458
|
+
|
|
459
|
+
this.bodyDef = {
|
|
460
|
+
type: "dynamic",
|
|
461
|
+
shapes: [new Rect(0, 0, 64, 96)],
|
|
462
|
+
collisionType: collision.types.PLAYER_OBJECT,
|
|
463
|
+
maxVelocity: { x: MAX_VEL_X, y: MAX_VEL_Y },
|
|
464
|
+
frictionAir: { x: 0.4, y: 0 },
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
input.bindKey(input.KEY.LEFT, "left");
|
|
468
|
+
input.bindKey(input.KEY.RIGHT, "right");
|
|
469
|
+
input.bindKey(input.KEY.UP, "jump", true);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
update(dt: number) {
|
|
473
|
+
const adapter = this.parentApp.world.adapter;
|
|
474
|
+
const vel = adapter.getVelocity(this);
|
|
475
|
+
|
|
476
|
+
if (input.isKeyPressed("left")) {
|
|
477
|
+
adapter.applyForce(this, { x: -MAX_VEL_X, y: 0 });
|
|
478
|
+
} else if (input.isKeyPressed("right")) {
|
|
479
|
+
adapter.applyForce(this, { x: MAX_VEL_X, y: 0 });
|
|
480
|
+
}
|
|
481
|
+
if (input.isKeyPressed("jump")) {
|
|
482
|
+
adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return super.update(dt);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
onCollisionStart(response, other) {
|
|
489
|
+
if (other.body.collisionType !== collision.types.ENEMY_OBJECT) return;
|
|
490
|
+
const adapter = this.parentApp.world.adapter;
|
|
491
|
+
if (response.normal.y < -0.7) {
|
|
492
|
+
// I'm on top of the enemy — stomp
|
|
493
|
+
const vel = adapter.getVelocity(this);
|
|
494
|
+
adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y * 0.75 });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.hurt();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**After — `@melonjs/matter-adapter`:**
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
import { Application, collision, input, Rect, Sprite, video } from "melonjs";
|
|
506
|
+
import { MatterAdapter } from "@melonjs/matter-adapter";
|
|
507
|
+
|
|
508
|
+
new Application(800, 600, {
|
|
509
|
+
parent: "screen",
|
|
510
|
+
renderer: video.AUTO,
|
|
511
|
+
// (1) Swap the adapter. Pick a gravity that suits your sprite scale.
|
|
512
|
+
physic: new MatterAdapter({ gravity: { x: 0, y: 5 } }),
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const MAX_VEL_X = 3;
|
|
516
|
+
const MAX_VEL_Y = 15;
|
|
517
|
+
// (2) Matter forces are Newtonian (force/mass*dt²) — magnitudes ~100× smaller.
|
|
518
|
+
const WALK_FORCE = 0.012;
|
|
519
|
+
|
|
520
|
+
class Player extends Sprite {
|
|
521
|
+
constructor(x: number, y: number) {
|
|
522
|
+
super(x, y, { image: "player", framewidth: 64, frameheight: 96 });
|
|
523
|
+
|
|
524
|
+
// bodyDef is portable — shape, collision type, maxVelocity unchanged.
|
|
525
|
+
// (3) frictionAir is scalar in matter (no per-axis variant).
|
|
526
|
+
this.bodyDef = {
|
|
527
|
+
type: "dynamic",
|
|
528
|
+
shapes: [new Rect(0, 0, 64, 96)],
|
|
529
|
+
collisionType: collision.types.PLAYER_OBJECT,
|
|
530
|
+
maxVelocity: { x: MAX_VEL_X, y: MAX_VEL_Y },
|
|
531
|
+
frictionAir: 0.02,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
input.bindKey(input.KEY.LEFT, "left");
|
|
535
|
+
input.bindKey(input.KEY.RIGHT, "right");
|
|
536
|
+
input.bindKey(input.KEY.UP, "jump", true);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
update(dt: number) {
|
|
540
|
+
const adapter = this.parentApp.world.adapter;
|
|
541
|
+
const vel = adapter.getVelocity(this);
|
|
542
|
+
|
|
543
|
+
// (4) Same applyForce calls — only the magnitude changed.
|
|
544
|
+
if (input.isKeyPressed("left")) {
|
|
545
|
+
adapter.applyForce(this, { x: -WALK_FORCE, y: 0 });
|
|
546
|
+
} else if (input.isKeyPressed("right")) {
|
|
547
|
+
adapter.applyForce(this, { x: WALK_FORCE, y: 0 });
|
|
548
|
+
}
|
|
549
|
+
// (5) setVelocity is portable — works the same way on both adapters.
|
|
550
|
+
if (input.isKeyPressed("jump")) {
|
|
551
|
+
adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return super.update(dt);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// (6) Stomp logic is unchanged — `response.normal` and `onCollisionStart`
|
|
558
|
+
// are part of the portable API and behave identically on both adapters.
|
|
559
|
+
onCollisionStart(response, other) {
|
|
560
|
+
if (other.body.collisionType !== collision.types.ENEMY_OBJECT) return;
|
|
561
|
+
if (response.normal.y < -0.7) {
|
|
562
|
+
const adapter = this.parentApp.world.adapter;
|
|
563
|
+
const vel = adapter.getVelocity(this);
|
|
564
|
+
adapter.setVelocity(this, { x: vel.x, y: -MAX_VEL_Y * 0.75 });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.hurt();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**Summary of changes:**
|
|
573
|
+
|
|
574
|
+
| # | Change | Reason |
|
|
575
|
+
|---|---|---|
|
|
576
|
+
| 1 | Pass `MatterAdapter` to `physic` | switching engines |
|
|
577
|
+
| 2 | `WALK_FORCE = 0.012` (down from `3`) | Newtonian magnitudes are much smaller |
|
|
578
|
+
| 3 | `frictionAir: 0.02` (scalar) | matter has no per-axis air friction |
|
|
579
|
+
| 4 | `applyForce` calls unchanged | portable API |
|
|
580
|
+
| 5 | `setVelocity` for jump unchanged | portable; correct pattern for instant velocity change |
|
|
581
|
+
| 6 | `onCollisionStart` + `response.normal` unchanged | portable lifecycle handler with identical contract on both adapters |
|
|
582
|
+
|
|
583
|
+
`bodyDef` shape, collision masks, max-velocity cap, key bindings, sprite setup, collision handler — all unchanged.
|
|
584
|
+
|
|
585
|
+
### Default behaviour differences vs builtin
|
|
586
|
+
|
|
587
|
+
| Behaviour | Builtin | Matter |
|
|
588
|
+
|---|---|---|
|
|
589
|
+
| `fixedRotation` default | n/a (SAT bodies don't rotate) | `true` — matches SAT. Set `false` in `bodyDef` to enable rotation. |
|
|
590
|
+
| Continuous collision detection | ❌ | ✅ |
|
|
591
|
+
| Sleeping bodies | ❌ | ✅ |
|
|
592
|
+
| Constraints (springs, joints) | ❌ | ✅ via `Matter.Constraint` (reach via `adapter.engine`) |
|
|
593
|
+
| `applyForce` units | px/frame² | matter Newtonian |
|
|
594
|
+
|
|
595
|
+
## Porting checklist
|
|
596
|
+
|
|
597
|
+
1. Verify the boot banner shows `physic: @melonjs/matter-adapter`
|
|
598
|
+
2. Set a reasonable `gravity` (e.g. `{ x: 0, y: 4 }` for arcade platformers)
|
|
599
|
+
3. Divide your existing `applyForce` magnitudes by ~30–100 as a starting point
|
|
600
|
+
4. Use `response.normal.y < -0.7` for stomp checks instead of `!isGrounded` (matter-native MTV; portable to builtin too)
|
|
601
|
+
5. Mark trigger / one-way / pickup bodies as `isSensor: true` to disable physical resolution while still firing the lifecycle handlers
|
|
602
|
+
6. Replace slope-response hacks with snap-to-surface in `onCollisionActive`
|
|
603
|
+
7. Convert TMX polylines to thin rectangles at load time
|
|
604
|
+
8. Pass `fixedRotation: true` in `bodyDef` for anything that should stay axis-aligned
|
|
605
|
+
|
|
606
|
+
## Examples
|
|
607
|
+
|
|
608
|
+
The melonJS repo's `platformer-matter` example is a full port of the canonical platformer running on this adapter, including:
|
|
609
|
+
|
|
610
|
+
- Matter-native player movement (`applyForce` for walking, `setVelocity` for the jump impulse + `applyForce` for the variable-height hold)
|
|
611
|
+
- Velocity-based stomp detection
|
|
612
|
+
- Slope-grip via `onCollisionActive`
|
|
613
|
+
- One-way platforms via `setSensor` + manual landing snap
|
|
614
|
+
- A live `physic:` line in the boot banner showing this adapter was loaded
|
|
615
|
+
|
|
616
|
+
## License
|
|
617
|
+
|
|
618
|
+
[MIT](https://github.com/melonjs/melonJS/blob/master/LICENSE)
|