melonjs 20.13.0 → 20.14.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.
Files changed (120) hide show
  1. package/README.md +65 -22
  2. package/build/application/application.d.ts +6 -1
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/header.d.ts.map +1 -1
  5. package/build/application/resize.d.ts.map +1 -1
  6. package/build/application/settings.d.ts +13 -8
  7. package/build/application/settings.d.ts.map +1 -1
  8. package/build/audio/audio.d.ts +8 -4
  9. package/build/audio/audio.d.ts.map +1 -1
  10. package/build/audio/backend/engine.d.ts.map +1 -1
  11. package/build/audio/playback.d.ts +2 -2
  12. package/build/audio/playback.d.ts.map +1 -1
  13. package/build/audio/spatial.d.ts.map +1 -1
  14. package/build/audio/types.d.ts +15 -5
  15. package/build/audio/types.d.ts.map +1 -1
  16. package/build/camera/camera2d.d.ts +10 -9
  17. package/build/camera/camera2d.d.ts.map +1 -1
  18. package/build/camera/camera3d.d.ts +11 -8
  19. package/build/camera/camera3d.d.ts.map +1 -1
  20. package/build/geometries/path2d.d.ts +9 -8
  21. package/build/geometries/path2d.d.ts.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +40993 -39738
  25. package/build/index.js.map +4 -4
  26. package/build/input/keyboard.d.ts.map +1 -1
  27. package/build/input/pointerevent.d.ts.map +1 -1
  28. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  29. package/build/level/level.d.ts.map +1 -1
  30. package/build/level/tiled/TMXLayer.d.ts +2 -1
  31. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  32. package/build/level/tiled/TMXTileMap.d.ts +13 -3
  33. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  34. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  35. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  36. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  37. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -0
  38. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  39. package/build/level/tiled/renderer/TMXRenderer.d.ts +7 -3
  40. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  41. package/build/lighting/light3d.d.ts +55 -0
  42. package/build/lighting/light3d.d.ts.map +1 -1
  43. package/build/loader/cache.d.ts +146 -97
  44. package/build/loader/cache.d.ts.map +1 -1
  45. package/build/loader/internal-types.d.ts +69 -0
  46. package/build/loader/internal-types.d.ts.map +1 -0
  47. package/build/loader/loader.d.ts +584 -292
  48. package/build/loader/loader.d.ts.map +1 -1
  49. package/build/loader/parsers/audio.d.ts.map +1 -1
  50. package/build/loader/parsers/fontface.d.ts.map +1 -1
  51. package/build/loader/parsers/gltf.d.ts.map +1 -1
  52. package/build/loader/parsers/tmx.d.ts.map +1 -1
  53. package/build/math/color.d.ts +4 -0
  54. package/build/math/color.d.ts.map +1 -1
  55. package/build/math/noise.d.ts +16 -16
  56. package/build/math/noise.d.ts.map +1 -1
  57. package/build/physics/adapter.d.ts +64 -25
  58. package/build/physics/adapter.d.ts.map +1 -1
  59. package/build/physics/builtin/body.d.ts +9 -0
  60. package/build/physics/builtin/body.d.ts.map +1 -1
  61. package/build/physics/builtin/builtin-adapter.d.ts +25 -3
  62. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  63. package/build/physics/builtin/detector.d.ts.map +1 -1
  64. package/build/physics/builtin/raycast.d.ts.map +1 -1
  65. package/build/physics/world.d.ts +40 -10
  66. package/build/physics/world.d.ts.map +1 -1
  67. package/build/renderable/container.d.ts +1 -0
  68. package/build/renderable/container.d.ts.map +1 -1
  69. package/build/renderable/draggable.d.ts.map +1 -1
  70. package/build/renderable/frameAnimation.d.ts.map +1 -1
  71. package/build/renderable/instanced_mesh.d.ts +16 -14
  72. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  73. package/build/renderable/mesh.d.ts +62 -5
  74. package/build/renderable/mesh.d.ts.map +1 -1
  75. package/build/renderable/trigger.d.ts +5 -0
  76. package/build/renderable/trigger.d.ts.map +1 -1
  77. package/build/renderable/ui/uibaseelement.d.ts +8 -1
  78. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  79. package/build/renderable/ui/uicheckbox.d.ts.map +1 -1
  80. package/build/system/device.d.ts +11 -2
  81. package/build/system/device.d.ts.map +1 -1
  82. package/build/system/fullscreen.d.ts.map +1 -1
  83. package/build/system/pool.d.ts.map +1 -1
  84. package/build/utils/utils.d.ts.map +1 -1
  85. package/build/video/canvas/canvas_renderer.d.ts +16 -14
  86. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  87. package/build/video/canvas_factory.d.ts.map +1 -1
  88. package/build/video/effects/shadereffect.d.ts +127 -0
  89. package/build/video/effects/shadereffect.d.ts.map +1 -1
  90. package/build/video/renderer.d.ts.map +1 -1
  91. package/build/video/webgl/batchers/lit_mesh_batcher.d.ts.map +1 -1
  92. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  93. package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
  94. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/skills/melonjs/SKILL.md +38 -14
  97. package/skills/melonjs-20-migration/SKILL.md +50 -7
  98. package/skills/melonjs-3d/SKILL.md +138 -38
  99. package/skills/melonjs-3d-assets/SKILL.md +83 -33
  100. package/skills/melonjs-audio/SKILL.md +55 -14
  101. package/skills/melonjs-camera-and-drawing/SKILL.md +95 -8
  102. package/skills/melonjs-deployment/SKILL.md +92 -14
  103. package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
  104. package/skills/melonjs-effects-and-shaders/SKILL.md +46 -15
  105. package/skills/melonjs-events/SKILL.md +3 -2
  106. package/skills/melonjs-getting-started/SKILL.md +5 -0
  107. package/skills/melonjs-input/SKILL.md +65 -15
  108. package/skills/melonjs-lighting/SKILL.md +13 -1
  109. package/skills/melonjs-loading-assets/SKILL.md +202 -27
  110. package/skills/melonjs-math/SKILL.md +12 -5
  111. package/skills/melonjs-particles-and-trails/SKILL.md +7 -3
  112. package/skills/melonjs-performance/SKILL.md +18 -8
  113. package/skills/melonjs-physics/SKILL.md +385 -127
  114. package/skills/melonjs-plugins/SKILL.md +45 -11
  115. package/skills/melonjs-renderables/SKILL.md +24 -7
  116. package/skills/melonjs-renderer-backends/SKILL.md +38 -17
  117. package/skills/melonjs-scenes-and-state/SKILL.md +30 -5
  118. package/skills/melonjs-sprites-and-animation/SKILL.md +23 -2
  119. package/skills/melonjs-tilemaps/SKILL.md +56 -18
  120. package/skills/melonjs-ui-and-text/SKILL.md +120 -25
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: melonjs-physics
3
- description: "Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT."
3
+ description: "Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck, matter and Rapier adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, switching adapters, setAdapter, PhysicsAdapterClass, create, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, raycast3d, PlanckAdapter, MatterAdapter, RapierAdapter, rapier2d, enableCcd, gravity, velocity, applyForce, isGrounded, isSensor, isTrigger, onShapeCollisionStart, kinematic, subSteps, pixelsPerMeter, Box3d, Sphere, getBodyShapes, hitbox offset, slope, SAT."
4
4
  license: MIT
5
5
  ---
6
6
 
7
7
  # Physics and collision
8
8
 
9
- > melonJS has a built-in SAT collision world and two optional rigid-body
9
+ > melonJS has a built-in SAT collision world and three optional rigid-body
10
10
  > adapters. Movement, collision callbacks and spatial queries all behave
11
11
  > differently depending on which is active — and several of the differences fail
12
12
  > silently.
@@ -18,6 +18,7 @@ license: MIT
18
18
  | **built-in** (default) | Arcade-style: platformers, top-down games. Fast, position-based, not Newtonian. |
19
19
  | **`@melonjs/planck-adapter`** | Full rigid-body dynamics — stacking, joints, realistic restitution. |
20
20
  | **`@melonjs/matter-adapter`** | Same class of thing, backed by matter-js. |
21
+ | **`@melonjs/rapier2d-adapter`** | Same again, backed by Rapier compiled to WebAssembly: many bodies, per-body continuous collision, a character controller. Loaded asynchronously. |
21
22
 
22
23
  ### Picking one
23
24
 
@@ -57,22 +58,50 @@ push-out, or you are shipping to a tight bundle budget. Tiled maps, `Trigger`
57
58
  and `Collectable` all work on every backend, so this is not a fork in the road
58
59
  for level content.
59
60
 
60
- **planck or matter?** Both are real rigid-body engines and either will do; the
61
- adapters expose the same portable API, so a game can switch with an import
62
- change and a gravity re-tune.
61
+ **planck, matter or Rapier?** All three are real rigid-body engines and any
62
+ will do; the adapters expose the same portable API, so a game can switch with
63
+ an import change and a gravity re-tune.
63
64
 
64
65
  - **planck** (a Box2D port) is the more accurate and the more predictable under
65
- stress: better stacking, better joint behaviour, a continuous-collision
66
- `bullet` mode for fast movers. It works in **metres**, so it has a
67
- `pixelsPerMeter` scale to think about, and its tuning vocabulary is Box2D's.
66
+ stress: better stacking, better joint behaviour, continuous collision for
67
+ fast movers. Box2D's `bullet` mode is not a `bodyDef` field: set it on the
68
+ native body with `(this.body as PlanckAdapter.Body).setBullet(true)`. It
69
+ works in **metres**, so it has a `pixelsPerMeter` scale to think about, and
70
+ its tuning vocabulary is Box2D's.
68
71
  - **matter-js** works directly in **pixels**, which makes it the gentler
69
72
  introduction, and its compound bodies and constraints are pleasant to author.
70
- It is the softer solver: stacks settle less crisply and fast bodies need
71
- `subSteps`.
72
-
73
- If you have no preference and the game leans on stacking, joints or fast
74
- projectiles, take planck. If you want to be up and running with the least
75
- conversion to think about, take matter.
73
+ It is the softer solver: stacks settle less crisply, and it has no continuous
74
+ collision, so fast bodies need `subSteps`.
75
+ - **Rapier** (`@melonjs/rapier2d-adapter`) is a Rust engine compiled to
76
+ WebAssembly. It handles many bodies at once, has continuous collision per
77
+ body for fast projectiles (`(this.body as RapierAdapter.Body).enableCcd(true)`),
78
+ a kinematic character controller, and simulates a concave polygon exactly.
79
+ It works in **metres** like planck, with the same `pixelsPerMeter`. The cost
80
+ is the download: about 3 MB of WebAssembly, fetched and compiled on demand,
81
+ so the adapter must be made with `await RapierAdapter.create()`.
82
+
83
+ **By game type**, the same choice from the other end:
84
+
85
+ | game | adapter | why |
86
+ |---|---|---|
87
+ | tile platformer, jumps authored as curves | built-in | movement is designed, not simulated; a body resting on flat static ground is exact to the pixel |
88
+ | platformer with slopes the player stands still on | any rigid-body adapter | the built-in has no surface friction, so a body on an incline slides down it. Walking up one is your own code on matter and planck (the `platformer-matter` example snaps its player onto sensor slopes); Rapier's character controller does it, through Rapier's own API, not the portable one |
89
+ | platformer with moving platforms that carry the player | planck or Rapier | true kinematic bodies, moved by their velocity; the built-in and matter build a dynamic body for `kinematic` |
90
+ | top-down RPG or adventure, grid or tile puzzle | built-in | collision is "did these touch" plus a push-out; no dependency, smallest bundle |
91
+ | shoot-'em-up, bullet hell | built-in | bullets need detection, not a physical response |
92
+ | 2.5D with height: isometric, beat-'em-up lanes | built-in only | the only backend that takes `Box3d` and `Sphere`; the 2D engines refuse them |
93
+ | fighting game or brawler, hitboxes and hurtboxes on one body | built-in, planck or Rapier | matter ignores a shape's own `collisionType`, `collisionMask` and `isTrigger` |
94
+ | stacking, towers, catapult and destruction puzzles | planck, or Rapier for many pieces | an iterative solver keeps a pile crisp; Rapier is built for many bodies at once |
95
+ | pool, billiards, mini-golf: few bodies, a capped top speed | any rigid-body adapter, matter included | give matter enough `subSteps` that a ball moves less than its radius per sub-step: the `pool-matter` example's break, about 30 px a frame on 16 px balls, runs on 2 |
96
+ | pinball, physical bullets: bodies much faster than their own size, with no useful cap | planck or Rapier | continuous collision (planck's `setBullet`, Rapier's `enableCcd`); matter has none, and each sub-step is a full extra solve |
97
+ | vehicles, ragdolls, ropes, chains | planck or Rapier | joint solvers; on every adapter a joint is made through the engine's own API |
98
+ | casual physics toy, prototype, game jam | matter | it works in pixels, so there is nothing to convert; its softer stacking does not show |
99
+ | sandbox with lots of debris | Rapier | many bodies at once, concave pieces simulated exactly |
100
+ | playable ad, size-capped portal, instant load | built-in, not Rapier | Rapier downloads and compiles about 3 MB of WebAssembly |
101
+
102
+ One game can use more than one: `world.setAdapter()` swaps the backend between
103
+ screens, so a platformer on the built-in can run a physics mini-game on planck
104
+ (see [Switching adapters while the game runs](#switching-adapters-while-the-game-runs)).
76
105
 
77
106
  An adapter is an `Application` setting, not a plugin:
78
107
 
@@ -81,7 +110,7 @@ import { PlanckAdapter } from "@melonjs/planck-adapter";
81
110
 
82
111
  const app = new Application(800, 600, {
83
112
  parent: "screen",
84
- physic: new PlanckAdapter({
113
+ physic: await PlanckAdapter.create({
85
114
  gravity: { x: 0, y: 320 }, // pixels/s², default {x: 0, y: 320}
86
115
  pixelsPerMeter: 32, // default 32
87
116
  subSteps: 2, // default 1
@@ -90,14 +119,45 @@ const app = new Application(800, 600, {
90
119
  await app.init();
91
120
  ```
92
121
 
122
+ **Make an adapter with `await X.create(options)`.** Every adapter class has
123
+ the same static pair (`PhysicsAdapterClass`): `load()` resolves once the
124
+ adapter can be built, and `create()` waits for it, then constructs. The
125
+ builtin, matter and planck adapters are plain JavaScript, so their `load()`
126
+ resolves at once and `new X(options)` is equivalent and still supported; an
127
+ adapter whose engine is WebAssembly downloads and compiles it in `load()`, and
128
+ only `create()` works for it without a prior `load()`. Write `create()` and
129
+ the code is the same for every adapter.
130
+
131
+ The Rapier adapter is that case. `new RapierAdapter()` before its `load()` has
132
+ finished throws "Rapier is not loaded yet"; `RapierAdapter.loaded` says
133
+ whether it has. `load()` returns one shared promise, so start it early and the
134
+ download runs alongside the game's own assets:
135
+
136
+ ```js
137
+ const rapierReady = RapierAdapter.load(); // starts the download now
138
+ await loader.preload(assets);
139
+ await rapierReady;
140
+ app.world.setAdapter(new RapierAdapter());
141
+ ```
142
+
93
143
  `PlanckAdapter` also takes `velocityIterations` (default `8`) and
94
144
  `positionIterations` (default `3`). `MatterAdapter` takes a different set —
95
145
  `gravity` (default `{x: 0, y: 1}`, matter's own convention), `subSteps` and
96
146
  `matterEngineOptions`; it has no `pixelsPerMeter` because it already works in
97
- pixels. `subSteps` is the anti-tunnelling knob when per-frame motion exceeds a
98
- body radius. Pick **one** adapter per game; `physic: "none"` disables physics
99
- entirely — `world.step` skips the simulation and the world behaves as a pure
100
- scene graph.
147
+ pixels. `RapierAdapter` takes `gravity` (pixels/s², default `{x: 0, y: 320}`),
148
+ `pixelsPerMeter` (default `32`) and `subSteps` (default `1`); Rapier's own
149
+ solver settings stay writable on `adapter.world.integrationParameters`.
150
+
151
+ `subSteps` is the anti-tunnelling knob when per-frame motion exceeds a body
152
+ radius. matter and planck clear forces after every sub-step, so a force applied
153
+ once per frame acts on the first sub-step only: raising `subSteps` weakens
154
+ every `applyForce`, and forces need re-tuning after it changes. Rapier keeps a
155
+ force for the whole frame, across every sub-step, so its forces do not change.
156
+
157
+ Pick **one** adapter per game (it can be switched later, see
158
+ [Switching adapters while the game runs](#switching-adapters-while-the-game-runs));
159
+ `physic: "none"` disables physics entirely — `world.step` skips the simulation
160
+ and the world behaves as a pure scene graph.
101
161
 
102
162
  **Gravity is a different unit in every backend, and the defaults are NOT the
103
163
  same speed.** Each is "Earth-like" in its own convention, but measured against
@@ -105,19 +165,22 @@ each other by timing a 200px free fall in one scene:
105
165
 
106
166
  | backend | default | unit | measured fall |
107
167
  |---|---|---|---|
108
- | built-in | `0.98` | per FRAME, integrated against `timer.tick` | **~4450 px/s²** |
168
+ | built-in | `0.98` | pixels per STEP², 60 steps a second | **~3600 px/s²** |
109
169
  | matter | `{x: 0, y: 1}` | matter's own, after its internal `gravity.scale` | **~900 px/s²** |
110
170
  | planck | `320` | pixels per second² | **~330 px/s²** |
171
+ | rapier | `320` | pixels per second², as planck | **~320 px/s²** |
111
172
 
112
- That is a **thirteenfold spread**, so swapping the built-in world for an adapter
113
- and keeping everything else the same makes the whole game feel different. It is
114
- not a bug in either one: they simply do not share a unit.
173
+ That is a roughly **elevenfold spread**, so swapping the built-in world for an
174
+ adapter and keeping everything else the same makes the whole game feel
175
+ different. It is not a bug in either one: they simply do not share a unit.
115
176
 
116
- To match a backend you are porting FROM, pick a target in px/s² and convert:
117
- planck takes it directly; matter takes `target / 900`; the built-in takes
118
- `target / 4450 * 0.98`, because it applies gravity per frame rather than per
119
- second. Passing one number to two of them compares nothing: hand planck
120
- matter's `{x: 0, y: 1}` and it runs at a three-hundredth of Earth gravity.
177
+ To match a backend you are porting FROM, pick a target in px/s² and convert,
178
+ scaling each default by its measured fall: planck and Rapier take it directly;
179
+ matter takes `target / 900`; the built-in takes `0.98 * target / 3600`,
180
+ because it applies gravity per step, 60 steps a second, rather than per
181
+ second. Passing one number to two of them compares nothing:
182
+ hand planck matter's `{x: 0, y: 1}` and it runs at a three-hundredth of Earth
183
+ gravity.
121
184
 
122
185
  That mistake is quiet and it does not look like a gravity problem. Bodies fall
123
186
  slowly enough that a spawner keeping a fixed interval starts constructing them
@@ -128,7 +191,35 @@ shapes permanently fused at rest, which reads as a broken collision shape
128
191
  rather than as a bad gravity constant. Spawn clear of anything already there.
129
192
 
130
193
  `world.physic` carries the active adapter's label (`"builtin"`, `"planck"`,
131
- `"matter"`, `"none"`), so game code can branch without importing the class.
194
+ `"matter"`, `"rapier"`, `"none"`), so game code can branch without importing
195
+ the class.
196
+
197
+ ### Switching adapters while the game runs
198
+
199
+ `world.adapter` is read-only. `app.world.setAdapter(adapter)` replaces it, for
200
+ example to move from planck to matter between two screens:
201
+
202
+ ```ts
203
+ import { MatterAdapter } from "@melonjs/matter-adapter";
204
+
205
+ app.world.setAdapter(await MatterAdapter.create({ gravity: { x: 0, y: 1 } }));
206
+ ```
207
+
208
+ The old adapter is destroyed, and the new one builds every body in the world
209
+ again from its renderable's `bodyDef`, where the renderable stands.
210
+ `world.physic` takes the new adapter's label.
211
+
212
+ - Nothing else carries over: velocities, forces, contacts and joints start
213
+ from rest, and gravity is the new adapter's own, in its own unit (see the
214
+ table above).
215
+ - A body added with a direct `adapter.addBody()` call rather than a `bodyDef`
216
+ is not built again. A legacy `Body` without a `bodyDef` works on the
217
+ built-in adapter only.
218
+ - Called from a collision handler, the swap waits for the end of the physics
219
+ step, so `world.adapter` is still the old adapter for the rest of that
220
+ handler.
221
+ - Code that kept the old adapter in a field (`this.adapter = app.world.adapter`)
222
+ must read `app.world.adapter` again after the swap.
132
223
 
133
224
  ## Two ways to declare a body
134
225
 
@@ -158,26 +249,42 @@ takes them all out again, so moving a whole group with `other.addChild(group)`
158
249
  keeps each body simulated, once. A persistent container's children keep their
159
250
  bodies through `world.reset()` (a state change or `level.load`).
160
251
  Calling `adapter.addBody()` yourself registers the body but leaves the
161
- renderable out of the scene graph, so it integrates but never collides.
162
-
163
- **`fixedRotation` follows the engine, so bodies rotate.** Both matter and
164
- planck default it to `false`, and so do the adapters from matter-adapter 1.4.0
165
- and planck-adapter 1.5.0. Pass `fixedRotation: true` for anything that must
252
+ renderable out of the scene graph. On the built-in adapter that body is then
253
+ outside the broadphase, so it never collides, and it does not move either
254
+ unless the renderable has `alwaysUpdate` set. On matter, planck and Rapier the
255
+ body is in the engine's world and collides, but its renderable is never drawn.
256
+
257
+ `type` is `"static"`, `"dynamic"` or `"kinematic"`, but only planck and Rapier
258
+ have a kinematic body: moved by its velocity (`setVelocity`), not by gravity.
259
+ The built-in and matter adapters build `"kinematic"` as a dynamic body, which
260
+ falls under gravity. For a moving platform that works on every backend, use
261
+ `type: "static"` and move it with `app.world.adapter.setPosition()`.
262
+
263
+ **`fixedRotation` follows the engine, so bodies rotate.** matter, planck and
264
+ Rapier all default it to `false`, and so do their adapters (from matter-adapter
265
+ 1.4.0 and planck-adapter 1.5.0). Pass `fixedRotation: true` for anything that must
166
266
  stay upright, such as a platformer actor. Earlier adapter versions locked
167
267
  rotation unless told otherwise, which inverted the engine they wrap: bodies
168
268
  landed holding whatever angle they were given and no collision could turn them,
169
269
  while `setAngle()` still worked, so a body could look rotated while being
170
270
  unable to rotate.
171
271
 
172
- `frictionAir` damps **rotation as well as translation** on both adapters, so
272
+ `frictionAir` damps **rotation as well as translation** on matter, planck and
273
+ Rapier, so
173
274
  it is also what settles a spin. Left at `0` a body that starts turning never
174
- stops, which only became visible once rotation stopped being locked.
275
+ stops, which only became visible once rotation stopped being locked. The same
276
+ holds for `adapter.setFrictionAir()` at runtime (on planck, from
277
+ planck-adapter 1.7.0).
175
278
 
176
279
  Other portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),
177
280
  `friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,
178
- `isSensor`, `userData`. Not every adapter honours every one — the built-in
179
- adapter ignores `friction`, `fixedRotation` and `userData`, and maps
180
- `frictionAir` onto its per-axis `body.friction` damping vector.
281
+ `isSensor`, `userData`. Not every adapter honours every one. The built-in
282
+ adapter ignores `friction` and `fixedRotation`, and no adapter reads
283
+ `userData`. The built-in maps `frictionAir` onto its per-axis `body.friction`
284
+ vector, a per-step deceleration in pixels rather than the damping the adapters
285
+ use (a fraction per step on matter, a rate per second on planck and Rapier), so
286
+ the same number is not the same drag. The adapters average an `{x, y}` value
287
+ into one.
181
288
 
182
289
  **`new Body(this, shape)`** — the built-in-native API. Fine, but ties the entity
183
290
  to the built-in world.
@@ -185,8 +292,11 @@ to the built-in world.
185
292
  ### Per-shape filtering and triggers
186
293
 
187
294
  A shape in `shapes` may carry its own `collisionType`, `collisionMask`,
188
- `isTrigger` (default `false`) and `isActive` (default `true`). A shape can only
189
- **narrow** what its body allows — the body's type/mask are checked first.
295
+ `isTrigger` (default `false`) and `isActive` (default `true`). On the built-in
296
+ adapter a shape can only **narrow** what its body allows: the body's type/mask
297
+ are checked first. On planck and Rapier a shape's own `collisionType` /
298
+ `collisionMask` replace the body's for that shape, so they can widen it too.
299
+ matter filters per body and ignores them.
190
300
 
191
301
  ```js
192
302
  const feet = new Rect(0, 24, 32, 8);
@@ -197,10 +307,19 @@ torso.isTrigger = true; // detects hits, never gets pushed by them
197
307
  ```
198
308
 
199
309
  `isActive: false` removes the shape from the narrow phase *and* from raycasts
200
- without shrinking the body's bounds. Honoured by the built-in and planck
201
- adapters; the matter adapter filters per body and ignores them. Under planck the
202
- body-wide setters (`setCollisionType` / `setCollisionMask` / `setSensor`) write
203
- every fixture and so overwrite per-shape values.
310
+ without shrinking the body's bounds, on every backend. matter, planck and
311
+ Rapier read it only when the body is built, so after changing it call
312
+ `app.world.adapter.updateShape(renderable, renderable.bodyDef.shapes)`, which
313
+ works on every adapter. On those three `updateShape()` builds a new body: its
314
+ velocity carries over, its angle and any joints do not. The built-in also
315
+ reads `isActive` live, from the shapes the body holds; those are not always
316
+ the ones you authored, since a `Rect` is stored as a converted `Polygon`, so
317
+ toggle `body.getShape(i)` there.
318
+
319
+ `isTrigger` works on the built-in, planck and Rapier; matter ignores it, as it
320
+ does the per-shape filter. Under planck and Rapier the body-wide setters
321
+ (`setCollisionType` / `setCollisionMask` / `setSensor`) write every shape and
322
+ so overwrite per-shape values.
204
323
 
205
324
  For contacts reported per *shape pair* rather than per body pair, define
206
325
  `onShapeCollisionStart` / `onShapeCollisionActive` / `onShapeCollisionEnd`.
@@ -221,7 +340,7 @@ from `bodyDef.shapes` with `id` naming the body to read:
221
340
  this.bodyDef = { type: "dynamic", shapes: "shapes", id: "hotdog" };
222
341
  ```
223
342
 
224
- This works identically on the built-in, matter and planck adapters — the file is
343
+ This works identically on every adapter — the file is
225
344
  resolved into real shapes before any adapter is handed the definition. Circles
226
345
  and polygons both load, and a concave outline (exported already decomposed into
227
346
  convex pieces) becomes one `Polygon` per piece.
@@ -229,8 +348,9 @@ convex pieces) becomes one `Polygon` per piece.
229
348
  The density, friction and bounce the shapes were authored with are carried onto
230
349
  the body definition too, each honoured by whichever backends support it — the
231
350
  built-in adapter ignores contact `friction`, as it does for a hand-written
232
- definition. The format carries these per fixture, but only planck could honour
233
- that, so they reduce onto the body and a file whose fixtures disagree warns. An
351
+ definition. The format carries these per fixture, but the built-in and matter
352
+ cannot honour that, so they reduce onto the body and a file whose fixtures
353
+ disagree warns. An
234
354
  explicit field on the definition always wins:
235
355
 
236
356
  ```js
@@ -257,23 +377,32 @@ built-in-only spelling of this and are deprecated since 20.7.0.
257
377
 
258
378
  ### Which shapes each backend can actually simulate
259
379
 
260
- | shape | builtin | matter | planck |
261
- |---|---|---|---|
262
- | `Rect`, `Polygon` | exact | exact | exact, up to 12 vertices |
263
- | `RoundRect` | exact | exact (decomposed) | sampled down to 12 vertices |
264
- | `Ellipse` | exact | circle of the average radius | circle of the average radius |
265
- | `Line` | exact | thin quad along the segment | thin quad along the segment |
266
- | `Box3d`, `Sphere` | exact, and in 3D | **throws** | **throws** |
267
- | `Point` | supported | **throws** | **throws** |
380
+ | shape | builtin | matter | planck | rapier |
381
+ |---|---|---|---|---|
382
+ | `Rect` | exact | exact | exact | exact |
383
+ | convex `Polygon` | exact | exact | exact, up to 12 vertices | exact, all its points |
384
+ | concave `Polygon` | **wrong**: SAT is convex-only | exact, split into convex parts | its convex hull | exact, as triangles |
385
+ | `RoundRect` | exact | exact | sampled down to 12 vertices | exact, all its points |
386
+ | `Ellipse` | exact | circle of the average radius | circle of the average radius | circle of the average radius |
387
+ | `Line` | exact | thin quad along the segment | thin quad along the segment | exact segment on a static or kinematic body, thin quad on a dynamic one |
388
+ | `Box3d`, `Sphere` | exact, and in 3D | **throws** | **throws** | **throws** |
389
+ | `Point` | supported | **throws** | **throws** | **throws** |
268
390
 
269
391
  Two of those are worth knowing before you author geometry. Box2D caps a
270
392
  polygon's vertex count and says nothing when you go over: measured, a
271
393
  16-point outline comes back with 12 vertices and a 36-point one collapses
272
394
  from 70x36 to 47x18. A `RoundRect` carries 36 points, four corners of nine
273
395
  arc segments each, so it is the usual way to meet that cap; the adapter now
274
- samples it down and keeps the extent. Anything the 2D backends cannot express
275
- is refused loudly on both, rather than leaving a body with no collision
276
- geometry that silently never collides.
396
+ samples it down and keeps the extent. Rapier has no such cap. Anything the 2D
397
+ backends cannot express is refused loudly on each of them, rather than leaving a
398
+ body with no collision geometry that silently never collides.
399
+
400
+ A concave polygon is a third. A Tiled map is safe: the loader splits a
401
+ concave collision polygon into convex triangles before any backend sees it,
402
+ and a shape editor exports one already split. A concave `Polygon` you build
403
+ yourself is simulated exactly by matter (split into convex parts) and Rapier
404
+ (as triangles); the built-in tests it as if it were convex and planck fills it
405
+ in to its hull, so on those two split it into convex pieces yourself.
277
406
 
278
407
  A polygon may be wound either way: its edge normals are oriented outward from
279
408
  its signed area. Before 20.13 they pointed inward on a counter-clockwise
@@ -289,11 +418,13 @@ debug panel is the fastest way to see an approximation for what it is.
289
418
 
290
419
  A `Line` is two points and no area, and it is what Tiled emits for every
291
420
  polyline, so it is the natural way to author a slope or a strip of ground.
292
- Neither rigid-body engine has a segment primitive, so both simulate one as a
421
+ matter and planck have no segment primitive, so both simulate one as a
293
422
  thin quad following the segment (2px), which is what `getBodyShapes()` reports
294
423
  back. Before 20.7 each substituted something of its own instead: matter the
295
424
  line's bounding box, planck a one-metre square wherever the body happened to
296
- be. Neither logged anything.
425
+ be. Neither logged anything. Rapier has a segment, and uses it on a static or
426
+ kinematic body, which is what Tiled terrain is; on a dynamic body, where a
427
+ segment would carry no mass, it uses the same 2px quad.
297
428
 
298
429
  The builtin solver resolves a sloped segment too, and correctly: a body lands
299
430
  on it and SLIDES down it, holding a constant distance from the surface while
@@ -306,8 +437,8 @@ before coming to rest on the flat. (It used to escape through an arm instead;
306
437
  that was a separate defect in how a body was separated from a shape list, fixed
307
438
  in 20.7.)
308
439
 
309
- matter and planck both have real surface friction, so the same valley holds a
310
- body still.
440
+ matter, planck and Rapier all have real surface friction, so the same valley
441
+ holds a body still.
311
442
 
312
443
  **Several shapes on one body.** A Tiled polyline arrives as ONE body carrying
313
444
  one shape per segment, and until 20.7 a body resting on such a ground could
@@ -327,7 +458,7 @@ the surface height at its CURRENT x instead: riding the slope shows a constant
327
458
  offset while x advances.
328
459
 
329
460
  So segments collide on every backend. If you need a body to come to REST on a
330
- sloped one, use matter or planck, or damp it yourself: the builtin will slide
461
+ sloped one, use a rigid-body adapter, or damp it yourself: the builtin will slide
331
462
  it down the incline indefinitely. The `Line Collision` example draws the
332
463
  authored segment against the geometry each backend reports, which is the
333
464
  quickest way to see what a backend is really simulating.
@@ -425,7 +556,20 @@ this.body.setFriction(0.4, 0); // ground drag, none vertically
425
556
  `friction` here is per-axis per-step velocity damping, not a surface
426
557
  coefficient — see the built-in quirks below.
427
558
 
428
- Under matter and planck, `syncFromPhysics()` copies the engine body's position
559
+ These two are methods of the built-in `Body` and are not functions on matter,
560
+ planck or Rapier. The portable forms go through the adapter, or the
561
+ `bodyDef`'s `maxVelocity` / `frictionAir`:
562
+
563
+ ```js
564
+ app.world.adapter.setMaxVelocity(this, { x: 3, y: 15 });
565
+ app.world.adapter.setFrictionAir(this, { x: 0.4, y: 0 }); // the adapters average the axes
566
+ ```
567
+
568
+ A velocity cap is in the adapter's velocity unit: pixels per step on the
569
+ built-in and matter, pixels per second on planck and Rapier (see *Porting*
570
+ below).
571
+
572
+ Under matter, planck and Rapier, `syncFromPhysics()` copies the engine body's position
429
573
  back onto `renderable.pos` after every step, so a direct write is simply erased
430
574
  — no error. The built-in world integrates `pos` in place instead, so a write
431
575
  survives but fights collision resolution and jitters. To teleport, go through
@@ -459,24 +603,36 @@ interface Contacts {
459
603
  onCollisionStart?: (response: CollisionResponse, other: Renderable) => void;
460
604
  }
461
605
  (rock as Contacts).onCollisionStart = (response, other) => { … };
462
- ``` The legacy `onCollision` still exists and has a trap:
606
+ ```
607
+
608
+ How often the lifecycle fires depends on the adapter. The built-in fires each
609
+ hook once per body pair, per side, per step. matter, planck and Rapier report
610
+ contacts per SHAPE pair, so a body touching through two of its shapes gets two
611
+ calls (on Rapier a concave polygon is one shape per triangle), and matter fires
612
+ `onCollisionActive` once per sub-step; planck and Rapier fire it once per
613
+ frame. Guard a one-shot reaction (a pickup, a landing sound) with a flag of
614
+ your own.
615
+
616
+ The legacy `onCollision` still exists and has a trap:
463
617
 
464
618
  > On the built-in world, `onCollision` fires **twice per frame** for
465
619
  > dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT
466
620
  > response with a fixed `a`/`b` per pair. On the adapters it is routed to their
467
- > "still touching" phase and fires once per side.
621
+ > "still touching" phase and fires once per side. There it receives the same
622
+ > response as `onCollisionActive` (`a`, `b`, `normal`, `depth`, `pair`), so
623
+ > `overlap`, `overlapN` and `overlapV` are `undefined`.
468
624
 
469
625
  The legacy dedupe idiom is `if (this !== response.a) return;`. Defining
470
626
  `onCollisionActive` suppresses the legacy callback on that renderable (the
471
627
  "supersedes" rule, applied per side so `a` and `b` can migrate independently),
472
- and is deduped to once per pair per side per frame on every adapter.
628
+ on every adapter.
473
629
 
474
630
  **Return values only work on the legacy handler.** `return false` from
475
- `onCollision` skips the built-in SAT push-out; what `onCollisionStart` /
476
- `onCollisionActive` return is discarded. To opt a body out of the physical
477
- response, set `bodyDef.isSensor` / `body.setSensor(true)`, or make the shape a
478
- trigger. With no `onCollision` defined at all, push-out happens by default for
479
- dynamic non-sensor bodies.
631
+ `onCollision` skips the built-in SAT push-out; the other adapters ignore it, and
632
+ what `onCollisionStart` / `onCollisionActive` return is discarded everywhere.
633
+ To opt a body out of the physical response, set `bodyDef.isSensor` /
634
+ `body.setSensor(true)`, or make the shape a trigger. With no `onCollision`
635
+ defined at all, push-out happens by default for dynamic non-sensor bodies.
480
636
 
481
637
  The two handler families do **not** get the same response object:
482
638
 
@@ -487,15 +643,22 @@ The two handler families do **not** get the same response object:
487
643
  | contact axis | `response.overlapN` / `overlapV` | `response.normal` |
488
644
  | also carries | `aInB`, `bInA`, `indexShapeA/B`, `isTriggerContact` | `pair` (adapter-native, absent on built-in) |
489
645
 
646
+ The left-hand column is the built-in's. On matter, planck and Rapier
647
+ `onCollision` gets the right-hand one too. On Rapier `pair` is
648
+ `{ collider1, collider2 }`, two collider handles for `adapter.world.getCollider()`.
649
+
490
650
  On the modern response `overlap` / `overlapN` / `overlapV` are still present but
491
- `@deprecated` (and `undefined` under the matter adapter) — use `depth` and
492
- `normal`. `normal` is the direction the receiver must move to separate, in
493
- canvas coordinates: `normal.y < -0.7` means "push me up", i.e. I landed on top
494
- of `other`.
651
+ `@deprecated` (and `undefined` under every adapter but the built-in) — use
652
+ `depth` and `normal`. `depth` is in pixels on every adapter (on planck from
653
+ planck-adapter 1.7.0; before, it was always `0`). `normal` is the direction
654
+ the receiver must move to separate, in canvas coordinates: `normal.y < -0.7`
655
+ means "push me up", i.e. I landed on top of `other`.
495
656
 
496
657
  One portability wrinkle: the built-in adapter dispatches `onCollisionEnd` with
497
- `undefined` as the response; the matter and planck adapters pass a real one.
498
- Guard it.
658
+ `undefined` as the response; the other adapters pass a real one. Guard it. On
659
+ Rapier a sensor contact, and every `onCollisionEnd`, carries a `normal` of
660
+ `(0, 0)` and a `depth` of `0`: Rapier computes no contact for them, so read
661
+ only `other` there.
499
662
 
500
663
  ## Never mutate the world during contact dispatch
501
664
 
@@ -520,13 +683,19 @@ What actually happens per adapter:
520
683
  planck's `begin-contact` / `end-contact` events, which fire *while the world
521
684
  is locked*. Adding a renderable with a `bodyDef` there makes planck's
522
685
  `createBody` return `null` and the adapter then throws on the null handle.
686
+ Removing one there is safe from planck-adapter 1.7.0: the body leaves every
687
+ event and query at once and is destroyed when the step ends. Before that,
688
+ `removeChildNow()` left an invisible body behind that still collided.
523
689
  `onCollisionActive` is dispatched after the step and is the safe one.
524
690
  - **matter** — callbacks come from matter's `collisionStart` / `collisionActive`
525
691
  / `collisionEnd` events during `Engine.update`.
692
+ - **Rapier**: the adapter reads Rapier's events once its step has returned
693
+ and dispatches them after, so adding and removing bodies in any handler is
694
+ safe. A partner still touching a removed body gets its `onCollisionEnd`.
526
695
 
527
696
  ## Spatial queries
528
697
 
529
- `raycast` and `queryAABB` are implemented by all three adapters:
698
+ `raycast` and `queryAABB` are implemented by every adapter:
530
699
 
531
700
  ```js
532
701
  import { Rect, Vector2d } from "melonjs";
@@ -541,9 +710,19 @@ const inArea = app.world.adapter.queryAABB(new Rect(x, y, w, h));
541
710
 
542
711
  `querySphere(centre, radius)` (or `querySphere(sphere)`) is **built-in only**,
543
712
  and only active under a 3D broadphase — i.e. `world.sortOn === "depth"`, which
544
- `Camera3d` sets. So is `raycast3d`. Check
545
- `world.adapter.capabilities.raycasts3d` before calling either. `raycast` itself
546
- is capability-gated by `capabilities.raycasts`.
713
+ `Camera3d` sets. So is `raycast3d`. `world.adapter.capabilities.raycasts3d`
714
+ says whether the adapter has them at all, and it stays `true` on the built-in
715
+ under a 2D camera, where `raycast3d` returns `null` and `querySphere` returns
716
+ `[]`: check `world.sortOn === "depth"` as well. `raycast` itself is
717
+ capability-gated by `capabilities.raycasts`.
718
+
719
+ `queryAABB` does not return the same set everywhere. On the built-in it
720
+ returns every non-kinematic renderable whose bounds overlap the rectangle, with
721
+ a body or without; matter, planck and Rapier query their own world, so they
722
+ return only renderables that have a body. On every adapter a ray skips a body
723
+ it starts inside, so a body casting from its own centre does not hit itself
724
+ (before 20.14 the built-in and matter-adapter < 1.6.0 returned that body); on
725
+ Rapier it hits sensors like any other body.
547
726
 
548
727
  On the built-in adapter a 2D ray measures each shape the way the narrowphase
549
728
  measures it against a planar shape: polygons, `Rect`, `RoundRect` and `Line`
@@ -558,12 +737,15 @@ The legacy `collision.rayCast(line, result)` is a different API: it takes a
558
737
  `Line`, returns an **array** of every intersecting renderable, and only exists
559
738
  on the built-in path.
560
739
 
561
- Two prerequisites that produce empty results rather than errors:
740
+ Two prerequisites on the built-in adapter that produce empty results rather
741
+ than errors:
562
742
 
563
743
  - **`isKinematic` must be `false`** for a renderable to be in the broadphase at
564
744
  all (a `Body` sets this for you).
565
745
  - On the **built-in** adapter, the broadphase is cleared and rebuilt inside
566
746
  `world.update()`, so querying before the first update returns nothing.
747
+ Rapier behaves the same: it indexes bodies for queries when its world
748
+ steps, so a body added this frame is found from the next one on.
567
749
 
568
750
  ## 3D collision: `Box3d`, `Sphere` and the Z pushback
569
751
 
@@ -574,10 +756,11 @@ along **Z** as well as X and Y.
574
756
  ```js
575
757
  import { Box3d, Sphere } from "melonjs";
576
758
 
577
- // a box: centre offset from the body, then half-extents on each axis
578
- crate.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));
579
- // a sphere: centre offset, then one radius
580
- rock.body.addShape(new Sphere(0, 0, 0, 30));
759
+ // a box: its centre, measured from the frame's top-left corner like any
760
+ // shape, then its FULL width, height and depth (not half-extents)
761
+ crate.body.addShape(new Box3d(crate.width / 2, crate.height / 2, 0, 40, 60, 40));
762
+ // a sphere: its centre, then one radius
763
+ rock.body.addShape(new Sphere(rock.width / 2, rock.height / 2, 0, 30));
581
764
  ```
582
765
 
583
766
  Reach for these for anything moving in the XZ plane under a `Camera3d` — a
@@ -621,7 +804,7 @@ the mistake to avoid: correct for a box pair, and for a sphere it throws away
621
804
  the planar half of the contact.
622
805
 
623
806
  **Mixed pairs degrade to 2D.** A `Box3d` or a `Sphere` against a `Polygon`,
624
- `Rectangle`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and
807
+ `Rect`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and
625
808
  treats the planar shape as unbounded along Z, which is what keeps a sphere or
626
809
  box body colliding with an ordinary Tiled collision layer. `overlapZ` is zero
627
810
  for those, since only one side has a depth to resolve. Give BOTH sides a 3D
@@ -672,24 +855,28 @@ real rigid-body engine:
672
855
  not contact state — it says nothing about what a body is touching.
673
856
  - **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate
674
857
  `app.world.adapter.gravity` at runtime, or pass
675
- `physic: new BuiltinAdapter({ gravity })` to override it. `body.gravityScale`
858
+ `physic: await BuiltinAdapter.create({ gravity: new Vector2d(0, g) })` to override
859
+ it (a `Vector2d` here, where the adapters take `{x, y}`). `body.gravityScale`
676
860
  (default `1`) scales it per body; `0` disables gravity for that body and is
677
861
  the portable replacement for the deprecated `body.ignoreGravity`.
678
862
  - **`applyForce(x, y)` is linear only** unless you pass the optional
679
863
  `(pointX, pointY)` application point, which generates a torque
680
864
  `τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.
681
865
  `applyTorque(τ)` is the direct form. The point is in **world** coordinates,
682
- as on matter and planck, and the lever arm is measured from the centre of
866
+ as on the other adapters, and the lever arm is measured from the centre of
683
867
  the shapes where they collide: the renderable's drawn frame (so the anchor
684
868
  counts) plus any parent container's position. `renderable.getBounds()` is
685
869
  in that space, so `b.centerX, b.top` is the top centre.
686
870
  - **Force accumulators reset at end-of-step** — for every body, including
687
871
  static, paused and off-screen ones — so forces must be applied every frame to
688
872
  persist.
689
- - **`isGrounded` is flag-based, not contact-based** — the adapter returns
690
- `!body.falling && !body.jumping`, flags updated by the last resolved
691
- collision, not a live contact test. The DIRECTION of the push-out decides
692
- them: pushed up out of a floor while not rising is a landing, pushed down
873
+ - **`isGrounded` reads the last step's push-out, not a live contact test** —
874
+ `true` when a collision in the last step pushed the body up out of
875
+ something below it (a floor or a slope, not a wall) and neither
876
+ `body.falling` nor `body.jumping` is set. Before 20.14 it read the two flags
877
+ alone, so a body never stepped, or held still in mid-air with
878
+ `gravityScale` 0, read as grounded. The DIRECTION of the push-out decides
879
+ the flags: pushed up out of a floor while not rising is a landing, pushed down
693
880
  by a ceiling while rising (or `jumping`) ends the jump and falls, and a
694
881
  side contact, a body touched from above while standing, or a body with
695
882
  `gravityScale` 0 leaves them alone. Before 20.13 a magnitude test decided
@@ -712,7 +899,7 @@ real rigid-body engine:
712
899
  - **`addBody` throws on double-registration** for a renderable that is already
713
900
  adapter-managed.
714
901
 
715
- ### Rotation collides on planck and matter, but not on the built-in solver
902
+ ### Rotation collides on planck, matter and Rapier, but not on the built-in solver
716
903
 
717
904
  This is the sharpest behaviour difference between the adapters, and it is
718
905
  silent — nothing warns you.
@@ -720,10 +907,10 @@ silent — nothing warns you.
720
907
  | adapter | a rotating body |
721
908
  | --- | --- |
722
909
  | **builtin** | rotates **visually only**. `body.angle`, `setAngle()` and angular integration write `renderable.currentTransform`; the collision shapes are never rotated, so a spinning sprite collides as if it were upright |
723
- | **planck** / **matter** | rotates for real — the solver owns the body's transform and collision follows it |
910
+ | **planck** / **matter** / **rapier** | rotates for real — the solver owns the body's transform and collision follows it |
724
911
 
725
912
  So a turret, a swinging platform or a tumbling crate that must *collide*
726
- rotated needs planck or matter. Under the builtin solver, approximate it with a
913
+ rotated needs a rigid-body adapter. Under the builtin solver, approximate it with a
727
914
  shape that is close enough at every angle (a circle, or a square for
728
915
  90° steps), or rotate the shapes yourself with `body.rotate(angle)` — which
729
916
  does rotate them, unlike `setAngle()`.
@@ -741,19 +928,20 @@ Track it yourself if you need to read it back. Before 20.7 it also threw on a
741
928
  grown.
742
929
 
743
930
  `adapter.getBodyShapes(renderable)` reports whichever is true: the geometry
744
- the engine is actually simulating on planck and matter (matter-adapter 1.3.0 /
745
- planck-adapter 1.4.0 and later), unrotated shapes on the builtin solver because
931
+ the engine is actually simulating on planck, matter and Rapier (matter-adapter
932
+ 1.3.0 / planck-adapter 1.4.0 and later), unrotated shapes on the builtin solver because
746
933
  there they genuinely are. That is what the debug overlay draws, so the hitbox
747
934
  you see is the hitbox that collides — a sprite spinning inside a stationary red
748
935
  box on the builtin is the solver telling you the truth, not a drawing bug.
749
936
 
750
- On planck and matter that report comes from the engine rather than from your
937
+ On the adapters that report comes from the engine rather than from your
751
938
  definitions, so it shows the approximations they make: an `Ellipse` appears as
752
939
  the circle of average radius it is simulated as, a concave polygon as the
753
- convex pieces it was decomposed into, and a `Rect` as a `Polygon`. Your
940
+ convex parts matter splits it into, the triangles Rapier builds or the hull
941
+ planck fills it in to, and a `Rect` as a `Polygon`. Your
754
942
  authored shapes are untouched and still readable on `renderable.bodyDef.shapes`.
755
943
 
756
- **If you need rotation to affect collision, use planck or matter.** That is the
944
+ **If you need rotation to affect collision, use planck, matter or Rapier.** That is the
757
945
  dividing line: the builtin is a position-based solver whose SAT never reads
758
946
  `body.angle`, so rotation there is a visual property. `body.rotate(angle)` is
759
947
  the workaround, not a rotating body — it permanently moves the shapes, does not
@@ -768,70 +956,140 @@ Same call, different magnitude — these are the ones that waste an afternoon:
768
956
 
769
957
  - **The portable API converts for you, the native handle does not.**
770
958
  `adapter.setVelocity` / `body.setVelocity(x, y)` are in pixels on every
771
- adapter; reaching for the raw handle (`body.setLinearVelocity` on planck) puts
772
- you in metres per second, scaled by `pixelsPerMeter`.
773
- - **Time base differs.** Built-in `body.vel` is pixels *per frame*; planck and
774
- matter integrate in seconds. The same number is ~60× off.
959
+ adapter; reaching for the raw handle (`body.setLinearVelocity` on planck,
960
+ `body.setLinvel` on Rapier) puts you in metres per second, scaled by
961
+ `pixelsPerMeter`.
962
+ - **Time base differs.** Built-in `body.vel` and matter's velocity are pixels
963
+ *per step* (60 steps a second); planck's and Rapier's are pixels *per
964
+ second*, so the same number is about 60× off there. `maxVelocity` follows the
965
+ same split, and so does angular velocity: radians per step on the built-in
966
+ and matter, per second on planck and Rapier.
775
967
  - **Force magnitudes are not comparable** between adapters; re-tune rather than
776
- reuse. `applyForce` is a per-step accumulator everywhere (cleared each step,
777
- so call it every frame) — `applyImpulse` is the one-shot. Matter has no native
778
- impulse, so the adapter emulates it as `Δv = J / mass` and ignores the
779
- application point.
968
+ reuse. `applyForce` is cleared after each step, so call it every frame; on
969
+ matter and planck that step is a sub-step, which is why `subSteps` weakens it
970
+ there, while Rapier keeps it for the whole frame. `applyImpulse` is the
971
+ one-shot. planck and Rapier apply
972
+ `adapter.applyImpulse(renderable, impulse, point)` at its point. The built-in
973
+ and matter ignore the point; matter has no native impulse, so the adapter
974
+ emulates it as `Δv = J / mass`.
975
+ - **Pausing stops the whole simulation** on matter, planck and Rapier; the
976
+ built-in keeps moving a renderable with `updateWhenPaused`.
780
977
  - **`body.position` is the centroid in matter**, while `renderable.pos` is
781
- top-left in melonJS. The adapter stores a per-body offset so `renderable.pos`
782
- stays correct — but read `body.position` directly and you get the centroid.
783
- - **Degenerate (zero-area / collinear) polygons — a Tiled polyline, a `Line` —
784
- cannot be built by matter and fall back to their axis-aligned bounding box.**
785
- Any other shape type throws `unsupported shape type`.
786
- - **Ellipses become circles on matter**, using the average of the two radii.
978
+ wherever its `anchorPoint` puts it. The adapter stores a per-body offset so
979
+ `renderable.pos` stays correct — but read `body.position` directly and you
980
+ get the centroid.
981
+ - **The default collision type differs.** With no `collisionType`, a body is
982
+ `ENEMY_OBJECT` on the built-in but category `1` (`PLAYER_OBJECT`) on matter,
983
+ planck and Rapier. With no `collisionMask`, it collides with every type on
984
+ every adapter (before planck-adapter 1.7.0, planck stopped at bit 15, so a
985
+ user type from `collision.types.USER << 9` up never hit it; Rapier filters on
986
+ the full 32 bits). Set `collisionType` on every `bodyDef`.
987
+ - **`type: "kinematic"` is planck and Rapier only**; the built-in and matter
988
+ build a dynamic body (see *Two ways to declare a body*).
989
+ - **A `Line` (what Tiled emits for a polyline) is a thin 2px rectangle along
990
+ the segment on matter**, which is what `getBodyShapes()` reports (since
991
+ matter-adapter 1.5.0; before that it fell back to its axis-aligned bounding
992
+ box). Only a degenerate (zero-area, collinear) `Polygon` still falls back to
993
+ its bounding box. Any other shape type throws `unsupported shape type`.
994
+ - **Ellipses become circles on matter, planck and Rapier**, using the average
995
+ of the two radii.
787
996
  - **`isGrounded` is capability-gated** (`capabilities.isGrounded`) and computed
788
- differently per adapter: a flag read on built-in, a live scan of active
789
- contacts on matter.
997
+ differently per adapter: the last step's push-out on built-in, a live scan of
998
+ active contacts on matter, planck and Rapier, counting a solid contact that
999
+ pushes the body up (a sensor contact counts when the other body is below).
1000
+ On every adapter the side of a wall is not ground.
790
1001
 
791
1002
  Adapter-specific escapes exist (`(this.body as MatterAdapter.Body).frictionAir`,
792
1003
  `adapter.matter.Constraint.create(...)`) but are not portable — flag them if you
793
1004
  use them. `adapter.capabilities` (`constraints`,
794
1005
  `continuousCollisionDetection`, `sleepingBodies`, `raycasts`, `raycasts3d`,
795
- `velocityLimit`, `isGrounded`) is the portable way to branch.
1006
+ `velocityLimit`, `isGrounded`) is the portable way to branch. matter reports
1007
+ `continuousCollisionDetection: false` (from matter-adapter 1.6.0; earlier
1008
+ versions claimed `true`): it has none, and `subSteps` is the remedy for fast
1009
+ bodies. Its `sleepingBodies` needs `matterEngineOptions: { enableSleeping: true }`.
1010
+ Rapier reports `continuousCollisionDetection: true`, but it is opt-in per body.
1011
+
1012
+ Rapier's escapes are `adapter.rapier` (the Rapier module) and `adapter.world`
1013
+ (the Rapier world), with the body typed as `RapierAdapter.Body`:
1014
+
1015
+ ```ts
1016
+ const adapter = app.world.adapter as RapierAdapter;
1017
+ const { rapier, world } = adapter;
1018
+ (bullet.body as RapierAdapter.Body).enableCcd(true); // continuous collision
1019
+ const hinge = rapier.JointData.revolute({ x: 0, y: 0 }, { x: 0, y: 0 }); // metres, body-local
1020
+ world.createImpulseJoint(hinge, door.body as RapierAdapter.Body, frame.body as RapierAdapter.Body, true);
1021
+ const controller = world.createCharacterController(0.01); // 0.01 m skin
1022
+ ```
1023
+
1024
+ The other joints are `JointData.fixed`, `spring`, `rope` and `prismatic`.
1025
+ Rapier's objects live in WebAssembly memory, which the garbage collector does
1026
+ not reclaim. The adapter's `destroy()`, which `setAdapter()` calls on the
1027
+ adapter it replaces, frees its world; free a joint or a character controller
1028
+ you made yourself with `world.removeImpulseJoint()` /
1029
+ `world.removeCharacterController()`.
796
1030
 
797
1031
  ## Symptom → cause
798
1032
 
799
1033
  | symptom | cause |
800
1034
  |---|---|
801
1035
  | entity jitters or does not move | assigning `pos` instead of applying force |
1036
+ | `world.adapter = …` throws a `TypeError` | `world.adapter` is read-only; call `world.setAdapter(adapter)` |
1037
+ | a body stops simulating after `world.setAdapter()` | it was added with a direct `adapter.addBody()`; give it a `bodyDef` |
802
1038
  | collision handler runs twice per frame | legacy `onCollision` on a dynamic pair (built-in) — use `onCollisionActive` |
803
1039
  | `return false` from a handler does nothing | return values are only honoured by legacy `onCollision` — use `isSensor` / `isTrigger` |
804
1040
  | crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |
805
1041
  | `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |
806
- | `querySphere` / `raycast3d` is not a function | 2D adapter, or built-in without a 3D broadphase (`sortOn !== "depth"`) |
1042
+ | `new RapierAdapter()` throws "Rapier is not loaded yet" | its WebAssembly loads asynchronously; use `await RapierAdapter.create(options)`, or `await RapierAdapter.load()` first |
1043
+ | a fast body passes through a thin one (Rapier) | continuous collision is per body and off by default: `(body as RapierAdapter.Body).enableCcd(true)` |
1044
+ | `normal` is `(0, 0)` and `depth` is `0` (Rapier) | a sensor contact or an `onCollisionEnd`: Rapier computes no contact there |
1045
+ | `querySphere` / `raycast3d` is not a function | matter, planck or Rapier: all are 2D-only and omit them |
1046
+ | `querySphere` returns `[]` and `raycast3d` returns `null` (built-in) | no 3D broadphase: `world.sortOn !== "depth"`; they need a `Camera3d` stage |
1047
+ | `queryAABB` misses objects that have no body (matter, planck, Rapier) | those adapters query their own world, which holds bodies only |
807
1048
  | a 3D contact reports no overlap to push against | read `overlapNZ` / `overlapZ` — a Z-axis MTV leaves `overlapN` / `overlapV` at zero |
808
1049
  | two 3D objects never separate in depth | one of them carries a planar shape; a mixed pair is tested as the box's XY footprint, unbounded along Z |
809
- | `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |
1050
+ | `response.depth` / `response.normal` are `undefined` (built-in) | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |
1051
+ | `response.overlapV` / `overlapN` are `undefined` (matter, planck, Rapier) | the adapters hand `onCollision` the modern response too; use `normal` / `depth` |
1052
+ | a pickup or a landing sound fires twice (matter, planck, Rapier) | contacts are reported per shape pair, and `onCollisionActive` once per sub-step on matter; guard with a flag |
1053
+ | a `type: "kinematic"` platform falls (built-in, matter) | only planck and Rapier have kinematic bodies; use `type: "static"` and `adapter.setPosition()` |
1054
+ | `this.body.setMaxVelocity is not a function` (matter, planck, Rapier) | it is a built-in `Body` method; use `app.world.adapter.setMaxVelocity(this, { x, y })` |
1055
+ | every force got weaker after raising `subSteps` (matter, planck) | forces are cleared after each sub-step, so they act on the first one only; re-tune (Rapier keeps a force for the whole frame) |
1056
+ | `isActive = false` on a shape changes nothing (matter, planck, Rapier) | read only when the body is built; call `adapter.updateShape(renderable, shapes)` |
810
1057
  | `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |
811
1058
  | off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |
812
1059
  | a body sinks through one shape of a multi-shape body | fixed in 20.7; before that the compound pass only ever re-resolved the first overlapping pair |
813
- | a body will not stay put on a slope on the builtin | it is riding the slope and sliding: the builtin has no surface friction, only `frictionAir` damping. Damp it yourself, or use matter/planck |
1060
+ | a body will not stay put on a slope on the builtin | it is riding the slope and sliding: the builtin has no surface friction, only `frictionAir` damping. Damp it yourself, or use a rigid-body adapter |
814
1061
  | the sprite is drawn offset from its hitbox | pre-20.7 `anchorPoint` moved the drawing but not the collision shapes; upgrade, and drop any offsets you added to compensate |
815
1062
  | a small shape sits at the sprite's corner | a shape's offset is measured from the drawn frame's TOP-LEFT; `new Sphere(0, 0, 0, r)` is the corner, `(width / 2, height / 2)` is the centre |
816
1063
  | a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |
817
- | a body in a pile sinks into the floor (built-in) | fixed in 20.7 for anything with an immovable side; bodies pinned only by other DYNAMIC bodies still overlap by a pixel or two, which is what planck/matter are for |
1064
+ | a body in a pile sinks into the floor (built-in) | fixed in 20.7 for anything with an immovable side; bodies pinned only by other DYNAMIC bodies still overlap by a pixel or two, which is what the rigid-body adapters are for |
818
1065
  | `applyForce` with a point spins a body pushed through its centre, or tips it the wrong way (built-in) | before 20.13 the point was measured against the body's LOCAL bounds, so only a body at the world origin got the right torque; upgrade, and drop any `pos` you subtracted to compensate |
819
1066
  | forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |
820
- | `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, melonJS the top-left — the adapter offsets between them |
1067
+ | `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, while `renderable.pos` sits where its `anchorPoint` puts it; the adapter offsets between them |
821
1068
  | a `bodyDef` two containers down never falls nor collides (`body` stays `undefined`) | before 20.13 only the added container's own children were registered; upgrade |
822
1069
  | `other.addChild(group)` throws "renderable is already adapter-managed" | before 20.13 a group detached with keepalive (which moving it does) left its children's bodies registered, still simulated off-stage; upgrade |
823
1070
  | the children of a persistent container stop falling and colliding after a state change or `level.load` | before 20.13 `world.reset()` kept only the bodies whose own renderable was persistent; upgrade, or flag each child `isPersistent` |
824
1071
  | `bodyDef.shapes: "…"` throws "is not loaded" | the name must match the `name` the JSON was preloaded under, not its filename |
825
1072
  | everything falls far too fast or too slow after swapping backends | gravity is a different unit per backend; see the table above |
826
1073
  | debris lands holding its spawn angle and never tumbles | `fixedRotation: true`, or matter-adapter < 1.4.0 / planck-adapter < 1.5.0 |
827
- | a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use planck/matter, or `body.rotate(angle)` |
1074
+ | a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use a rigid-body adapter, or `body.rotate(angle)` |
828
1075
  | `setAngle` swings a sprite away from where it stood (built-in) | before 20.13 the pivot ignored `anchorPoint`, so a centre-anchored renderable turned about its bottom-right corner; upgrade |
829
1076
  | `isGrounded()` stays `false` on the floor, so the player cannot jump (built-in) | before 20.13 a body sinking 1px or more per step (gravity or mass above ~1.02, `timer.tick > 1`, a rising floor) never landed; upgrade |
1077
+ | `isGrounded()` is `true` in mid-air, for a body with `gravityScale` 0 or one not yet stepped (built-in) | before 20.14 it read the `falling` / `jumping` flags alone, without a surface under the body; upgrade |
1078
+ | a ray cast from a body's centre hits that body (built-in, matter) | before 20.14 / matter-adapter 1.6.0 a ray did not skip a body it starts inside; upgrade |
1079
+ | `raycast()` / `queryAABB()` find nothing right after the bodies are added (built-in, Rapier) | both index bodies for queries during a world step: query from the next frame on |
830
1080
  | a circle body is shoved by a polygon it is nowhere near (built-in) | before 20.13 a counter-clockwise polygon (Tiled, mirrored with `scale(-1, 1)`) had inward edge normals; upgrade |
831
1081
  | `raycast` throws "reading 'x'" / "reading 'length'" (built-in) | before 20.13 a body carrying a `Point`, `Sphere` or `Box3d` broke the ray walk; upgrade |
832
1082
  | a ray passes through a rotated ellipse, or stops short of it (built-in) | before 20.13 the raycast ignored `Ellipse#angle`; upgrade |
833
1083
  | the debug hitbox does not follow a spinning body | pre-1.2.1 matter-adapter / pre-1.3.1 planck-adapter returned the authored shapes rather than the rotated ones |
834
- | a Tiled polyline becomes a solid box on matter | matter cannot build a zero-area polygon; the adapter falls back to its AABB |
1084
+ | a Tiled polyline becomes a solid box on matter | matter-adapter < 1.5.0 built a `Line` as its AABB; upgrade (a degenerate `Polygon` still falls back to one) |
1085
+ | the player climbs a tall wall with repeated jumps (matter, planck) | matter-adapter < 1.5.3 / planck-adapter < 1.6.3 counted any contact with a lower body as ground; upgrade |
1086
+ | a hero made of several shapes is never grounded, so it cannot jump (matter) | matter-adapter < 1.5.3 missed contacts on the parts of a compound body; upgrade |
1087
+ | `setSensor(true)` on a platform under the player does nothing, so it cannot be dropped through (matter) | matter-adapter < 1.5.3 did not update a contact already in place; upgrade |
1088
+ | `raycast` misses a body made of several shapes, or a concave one (matter) | matter-adapter < 1.6.0 did not resolve the part it hit to its body; upgrade |
1089
+ | a shape hook's `normal` points the opposite way to `onCollisionStart`'s (matter) | matter-adapter < 1.6.0 reversed it; upgrade |
1090
+ | `response.depth` is always `0` (planck) | planck-adapter < 1.7.0 read the depth from the wrong place; upgrade |
1091
+ | an invisible body keeps colliding after `removeChildNow()` from `onCollisionStart` (planck) | planck-adapter < 1.7.0 could not destroy a body while the world was locked; upgrade, or use `removeChild()` |
1092
+ | a spin never settles after `adapter.setFrictionAir()` (planck) | planck-adapter < 1.7.0 damped movement only there; upgrade |
835
1093
 
836
1094
  ## Related skills
837
1095