melonjs 20.12.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.
- package/README.md +66 -22
- package/build/application/application.d.ts +6 -1
- package/build/application/application.d.ts.map +1 -1
- package/build/application/header.d.ts.map +1 -1
- package/build/application/resize.d.ts.map +1 -1
- package/build/application/settings.d.ts +13 -8
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/audio.d.ts +8 -4
- package/build/audio/audio.d.ts.map +1 -1
- package/build/audio/backend/engine.d.ts.map +1 -1
- package/build/audio/playback.d.ts +2 -2
- package/build/audio/playback.d.ts.map +1 -1
- package/build/audio/spatial.d.ts.map +1 -1
- package/build/audio/types.d.ts +15 -5
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera2d.d.ts +12 -9
- package/build/camera/camera2d.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts +11 -8
- package/build/camera/camera3d.d.ts.map +1 -1
- package/build/camera/effects/fade_effect.d.ts.map +1 -1
- package/build/geometries/ellipse.d.ts +3 -0
- package/build/geometries/ellipse.d.ts.map +1 -1
- package/build/geometries/line.d.ts.map +1 -1
- package/build/geometries/path2d.d.ts +9 -8
- package/build/geometries/path2d.d.ts.map +1 -1
- package/build/geometries/polygon.d.ts +2 -1
- package/build/geometries/polygon.d.ts.map +1 -1
- package/build/index.d.ts +14 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +50215 -45051
- package/build/index.js.map +4 -4
- package/build/input/gamepad.d.ts.map +1 -1
- package/build/input/keyboard.d.ts.map +1 -1
- package/build/input/pointerevent.d.ts.map +1 -1
- package/build/level/gltf/GLTFModel.d.ts.map +1 -1
- package/build/level/level.d.ts.map +1 -1
- package/build/level/tiled/TMXLayer.d.ts +50 -15
- package/build/level/tiled/TMXLayer.d.ts.map +1 -1
- package/build/level/tiled/TMXTileMap.d.ts +14 -4
- package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
- package/build/level/tiled/TMXTileset.d.ts.map +1 -1
- package/build/level/tiled/TMXUtils.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -0
- package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXRenderer.d.ts +7 -3
- package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXStaggeredRenderer.d.ts.map +1 -1
- package/build/lighting/light3d.d.ts +55 -0
- package/build/lighting/light3d.d.ts.map +1 -1
- package/build/loader/cache.d.ts +146 -97
- package/build/loader/cache.d.ts.map +1 -1
- package/build/loader/internal-types.d.ts +69 -0
- package/build/loader/internal-types.d.ts.map +1 -0
- package/build/loader/loader.d.ts +589 -253
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/parsers/audio.d.ts.map +1 -1
- package/build/loader/parsers/fontface.d.ts.map +1 -1
- package/build/loader/parsers/gltf.d.ts.map +1 -1
- package/build/loader/parsers/tmx.d.ts.map +1 -1
- package/build/math/color.d.ts +4 -0
- package/build/math/color.d.ts.map +1 -1
- package/build/math/math.d.ts +6 -1
- package/build/math/math.d.ts.map +1 -1
- package/build/math/noise.d.ts +16 -16
- package/build/math/noise.d.ts.map +1 -1
- package/build/math/observableVector2d.d.ts +6 -6
- package/build/math/observableVector2d.d.ts.map +1 -1
- package/build/math/observableVector3d.d.ts +8 -8
- package/build/math/observableVector3d.d.ts.map +1 -1
- package/build/particles/emitter.d.ts +15 -3
- package/build/particles/emitter.d.ts.map +1 -1
- package/build/particles/particle.d.ts +6 -0
- package/build/particles/particle.d.ts.map +1 -1
- package/build/particles/settings.d.ts +22 -5
- package/build/particles/settings.d.ts.map +1 -1
- package/build/physics/adapter.d.ts +64 -25
- package/build/physics/adapter.d.ts.map +1 -1
- package/build/physics/builtin/body.d.ts +23 -4
- package/build/physics/builtin/body.d.ts.map +1 -1
- package/build/physics/builtin/builtin-adapter.d.ts +25 -3
- package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
- package/build/physics/builtin/detector.d.ts.map +1 -1
- package/build/physics/builtin/raycast.d.ts.map +1 -1
- package/build/physics/world.d.ts +40 -10
- package/build/physics/world.d.ts.map +1 -1
- package/build/renderable/container.d.ts +41 -6
- package/build/renderable/container.d.ts.map +1 -1
- package/build/renderable/draggable.d.ts.map +1 -1
- package/build/renderable/frameAnimation.d.ts.map +1 -1
- package/build/renderable/instanced_mesh.d.ts +16 -14
- package/build/renderable/instanced_mesh.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +62 -5
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite.d.ts +4 -2
- package/build/renderable/sprite.d.ts.map +1 -1
- package/build/renderable/text/bitmaptext.d.ts +12 -3
- package/build/renderable/text/bitmaptext.d.ts.map +1 -1
- package/build/renderable/text/text.d.ts +147 -4
- package/build/renderable/text/text.d.ts.map +1 -1
- package/build/renderable/text/textmetrics.d.ts +10 -2
- package/build/renderable/text/textmetrics.d.ts.map +1 -1
- package/build/renderable/text/textstyle.d.ts.map +1 -1
- package/build/renderable/trigger.d.ts +5 -0
- package/build/renderable/trigger.d.ts.map +1 -1
- package/build/renderable/ui/panelpaint.d.ts +5 -1
- package/build/renderable/ui/panelpaint.d.ts.map +1 -1
- package/build/renderable/ui/progressbar.d.ts +267 -95
- package/build/renderable/ui/progressbar.d.ts.map +1 -1
- package/build/renderable/ui/uibaseelement.d.ts +443 -50
- package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
- package/build/renderable/ui/uicheckbox.d.ts +383 -0
- package/build/renderable/ui/uicheckbox.d.ts.map +1 -0
- package/build/renderable/ui/uifocus.d.ts +6 -0
- package/build/renderable/ui/uifocus.d.ts.map +1 -0
- package/build/renderable/ui/uilabel.d.ts +31 -0
- package/build/renderable/ui/uilabel.d.ts.map +1 -0
- package/build/renderable/ui/uipanel.d.ts +589 -116
- package/build/renderable/ui/uipanel.d.ts.map +1 -1
- package/build/renderable/ui/uiskin.d.ts +25 -0
- package/build/renderable/ui/uiskin.d.ts.map +1 -0
- package/build/renderable/ui/uislider.d.ts +518 -0
- package/build/renderable/ui/uislider.d.ts.map +1 -0
- package/build/renderable/ui/uispriteelement.d.ts +161 -85
- package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
- package/build/renderable/ui/uitextbutton.d.ts +369 -63
- package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
- package/build/state/stage.d.ts.map +1 -1
- package/build/system/device.d.ts +11 -2
- package/build/system/device.d.ts.map +1 -1
- package/build/system/fullscreen.d.ts.map +1 -1
- package/build/system/pool.d.ts.map +1 -1
- package/build/system/save.d.ts +2 -1
- package/build/system/save.d.ts.map +1 -1
- package/build/tweens/tween.d.ts +5 -2
- package/build/tweens/tween.d.ts.map +1 -1
- package/build/utils/fetchdata.d.ts.map +1 -1
- package/build/utils/utils.d.ts.map +1 -1
- package/build/video/canvas/canvas_renderer.d.ts +16 -19
- package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
- package/build/video/canvas_factory.d.ts.map +1 -1
- package/build/video/effects/shadereffect.d.ts +127 -0
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/renderer.d.ts +20 -4
- package/build/video/renderer.d.ts.map +1 -1
- package/build/video/renderstate.d.ts +26 -3
- package/build/video/renderstate.d.ts.map +1 -1
- package/build/video/utils/tessellation.d.ts +11 -0
- package/build/video/utils/tessellation.d.ts.map +1 -1
- package/build/video/webgl/batchers/lit_mesh_batcher.d.ts.map +1 -1
- package/build/video/webgl/webgl_renderer.d.ts +3 -6
- package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
- package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
- package/build/video/webgpu/batchers/primitive_batcher.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +3 -6
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/melonjs/SKILL.md +41 -17
- package/skills/melonjs-20-migration/SKILL.md +50 -7
- package/skills/melonjs-3d/SKILL.md +138 -38
- package/skills/melonjs-3d-assets/SKILL.md +83 -33
- package/skills/melonjs-audio/SKILL.md +62 -19
- package/skills/melonjs-camera-and-drawing/SKILL.md +135 -8
- package/skills/melonjs-deployment/SKILL.md +92 -14
- package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
- package/skills/melonjs-effects-and-shaders/SKILL.md +47 -15
- package/skills/melonjs-events/SKILL.md +6 -3
- package/skills/melonjs-getting-started/SKILL.md +6 -2
- package/skills/melonjs-input/SKILL.md +102 -16
- package/skills/melonjs-lighting/SKILL.md +13 -1
- package/skills/melonjs-loading-assets/SKILL.md +255 -39
- package/skills/melonjs-math/SKILL.md +26 -8
- package/skills/melonjs-particles-and-trails/SKILL.md +77 -9
- package/skills/melonjs-performance/SKILL.md +18 -8
- package/skills/melonjs-physics/SKILL.md +435 -128
- package/skills/melonjs-plugins/SKILL.md +50 -11
- package/skills/melonjs-renderables/SKILL.md +31 -7
- package/skills/melonjs-renderer-backends/SKILL.md +38 -17
- package/skills/melonjs-scenes-and-state/SKILL.md +45 -5
- package/skills/melonjs-sprites-and-animation/SKILL.md +37 -2
- package/skills/melonjs-tilemaps/SKILL.md +149 -17
- package/skills/melonjs-ui-and-text/SKILL.md +674 -42
|
@@ -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
|
|
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
|
|
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
|
|
61
|
-
adapters expose the same portable API, so a game can switch with
|
|
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,
|
|
66
|
-
`bullet` mode
|
|
67
|
-
|
|
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
|
|
71
|
-
`subSteps`.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
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. `
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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 **
|
|
113
|
-
and keeping everything else the same makes the whole game feel
|
|
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
|
-
|
|
118
|
-
`target /
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
|
@@ -152,27 +243,48 @@ class Player extends Sprite {
|
|
|
152
243
|
The engine forwards it to the active adapter when the renderable is added with
|
|
153
244
|
`Container.addChild`, and only if that container is already attached to the root
|
|
154
245
|
world; the broadphase picks the renderable up on the next `world.update()`.
|
|
246
|
+
Nesting is fine: adding a container registers the body of every renderable
|
|
247
|
+
below it, at any depth, and removing it — destroyed, pooled or kept alive —
|
|
248
|
+
takes them all out again, so moving a whole group with `other.addChild(group)`
|
|
249
|
+
keeps each body simulated, once. A persistent container's children keep their
|
|
250
|
+
bodies through `world.reset()` (a state change or `level.load`).
|
|
155
251
|
Calling `adapter.addBody()` yourself registers the body but leaves the
|
|
156
|
-
renderable out of the scene graph
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
161
266
|
stay upright, such as a platformer actor. Earlier adapter versions locked
|
|
162
267
|
rotation unless told otherwise, which inverted the engine they wrap: bodies
|
|
163
268
|
landed holding whatever angle they were given and no collision could turn them,
|
|
164
269
|
while `setAngle()` still worked, so a body could look rotated while being
|
|
165
270
|
unable to rotate.
|
|
166
271
|
|
|
167
|
-
`frictionAir` damps **rotation as well as translation** on
|
|
272
|
+
`frictionAir` damps **rotation as well as translation** on matter, planck and
|
|
273
|
+
Rapier, so
|
|
168
274
|
it is also what settles a spin. Left at `0` a body that starts turning never
|
|
169
|
-
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).
|
|
170
278
|
|
|
171
279
|
Other portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),
|
|
172
280
|
`friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,
|
|
173
|
-
`isSensor`, `userData`. Not every adapter honours every one
|
|
174
|
-
adapter ignores `friction
|
|
175
|
-
`frictionAir` onto its per-axis `body.friction`
|
|
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.
|
|
176
288
|
|
|
177
289
|
**`new Body(this, shape)`** — the built-in-native API. Fine, but ties the entity
|
|
178
290
|
to the built-in world.
|
|
@@ -180,8 +292,11 @@ to the built-in world.
|
|
|
180
292
|
### Per-shape filtering and triggers
|
|
181
293
|
|
|
182
294
|
A shape in `shapes` may carry its own `collisionType`, `collisionMask`,
|
|
183
|
-
`isTrigger` (default `false`) and `isActive` (default `true`).
|
|
184
|
-
**narrow** what its body allows
|
|
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.
|
|
185
300
|
|
|
186
301
|
```js
|
|
187
302
|
const feet = new Rect(0, 24, 32, 8);
|
|
@@ -192,10 +307,19 @@ torso.isTrigger = true; // detects hits, never gets pushed by them
|
|
|
192
307
|
```
|
|
193
308
|
|
|
194
309
|
`isActive: false` removes the shape from the narrow phase *and* from raycasts
|
|
195
|
-
without shrinking the body's bounds
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
every
|
|
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.
|
|
199
323
|
|
|
200
324
|
For contacts reported per *shape pair* rather than per body pair, define
|
|
201
325
|
`onShapeCollisionStart` / `onShapeCollisionActive` / `onShapeCollisionEnd`.
|
|
@@ -216,7 +340,7 @@ from `bodyDef.shapes` with `id` naming the body to read:
|
|
|
216
340
|
this.bodyDef = { type: "dynamic", shapes: "shapes", id: "hotdog" };
|
|
217
341
|
```
|
|
218
342
|
|
|
219
|
-
This works identically on
|
|
343
|
+
This works identically on every adapter — the file is
|
|
220
344
|
resolved into real shapes before any adapter is handed the definition. Circles
|
|
221
345
|
and polygons both load, and a concave outline (exported already decomposed into
|
|
222
346
|
convex pieces) becomes one `Polygon` per piece.
|
|
@@ -224,8 +348,9 @@ convex pieces) becomes one `Polygon` per piece.
|
|
|
224
348
|
The density, friction and bounce the shapes were authored with are carried onto
|
|
225
349
|
the body definition too, each honoured by whichever backends support it — the
|
|
226
350
|
built-in adapter ignores contact `friction`, as it does for a hand-written
|
|
227
|
-
definition. The format carries these per fixture, but
|
|
228
|
-
that, so they reduce onto the body and a file whose fixtures
|
|
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
|
|
229
354
|
explicit field on the definition always wins:
|
|
230
355
|
|
|
231
356
|
```js
|
|
@@ -252,23 +377,39 @@ built-in-only spelling of this and are deprecated since 20.7.0.
|
|
|
252
377
|
|
|
253
378
|
### Which shapes each backend can actually simulate
|
|
254
379
|
|
|
255
|
-
| shape | builtin | matter | planck |
|
|
256
|
-
|
|
257
|
-
| `Rect
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
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** |
|
|
263
390
|
|
|
264
391
|
Two of those are worth knowing before you author geometry. Box2D caps a
|
|
265
392
|
polygon's vertex count and says nothing when you go over: measured, a
|
|
266
393
|
16-point outline comes back with 12 vertices and a 36-point one collapses
|
|
267
394
|
from 70x36 to 47x18. A `RoundRect` carries 36 points, four corners of nine
|
|
268
395
|
arc segments each, so it is the usual way to meet that cap; the adapter now
|
|
269
|
-
samples it down and keeps the extent. Anything the 2D
|
|
270
|
-
is refused loudly on
|
|
271
|
-
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.
|
|
406
|
+
|
|
407
|
+
A polygon may be wound either way: its edge normals are oriented outward from
|
|
408
|
+
its signed area. Before 20.13 they pointed inward on a counter-clockwise
|
|
409
|
+
polygon — a Tiled polygon drawn that way, anything mirrored with
|
|
410
|
+
`scale(-1, 1)` or a `Rect` of negative width — and the built-in circle test
|
|
411
|
+
then collided a circle with it from tens of pixels away, as soon as their
|
|
412
|
+
bounding boxes met. Polygon against polygon was never affected.
|
|
272
413
|
|
|
273
414
|
Whatever a backend really simulates is what `getBodyShapes()` reports, so the
|
|
274
415
|
debug panel is the fastest way to see an approximation for what it is.
|
|
@@ -277,11 +418,13 @@ debug panel is the fastest way to see an approximation for what it is.
|
|
|
277
418
|
|
|
278
419
|
A `Line` is two points and no area, and it is what Tiled emits for every
|
|
279
420
|
polyline, so it is the natural way to author a slope or a strip of ground.
|
|
280
|
-
|
|
421
|
+
matter and planck have no segment primitive, so both simulate one as a
|
|
281
422
|
thin quad following the segment (2px), which is what `getBodyShapes()` reports
|
|
282
423
|
back. Before 20.7 each substituted something of its own instead: matter the
|
|
283
424
|
line's bounding box, planck a one-metre square wherever the body happened to
|
|
284
|
-
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.
|
|
285
428
|
|
|
286
429
|
The builtin solver resolves a sloped segment too, and correctly: a body lands
|
|
287
430
|
on it and SLIDES down it, holding a constant distance from the surface while
|
|
@@ -294,8 +437,8 @@ before coming to rest on the flat. (It used to escape through an arm instead;
|
|
|
294
437
|
that was a separate defect in how a body was separated from a shape list, fixed
|
|
295
438
|
in 20.7.)
|
|
296
439
|
|
|
297
|
-
matter and
|
|
298
|
-
body still.
|
|
440
|
+
matter, planck and Rapier all have real surface friction, so the same valley
|
|
441
|
+
holds a body still.
|
|
299
442
|
|
|
300
443
|
**Several shapes on one body.** A Tiled polyline arrives as ONE body carrying
|
|
301
444
|
one shape per segment, and until 20.7 a body resting on such a ground could
|
|
@@ -315,7 +458,7 @@ the surface height at its CURRENT x instead: riding the slope shows a constant
|
|
|
315
458
|
offset while x advances.
|
|
316
459
|
|
|
317
460
|
So segments collide on every backend. If you need a body to come to REST on a
|
|
318
|
-
sloped one, use
|
|
461
|
+
sloped one, use a rigid-body adapter, or damp it yourself: the builtin will slide
|
|
319
462
|
it down the incline indefinitely. The `Line Collision` example draws the
|
|
320
463
|
authored segment against the geometry each backend reports, which is the
|
|
321
464
|
quickest way to see what a backend is really simulating.
|
|
@@ -413,7 +556,20 @@ this.body.setFriction(0.4, 0); // ground drag, none vertically
|
|
|
413
556
|
`friction` here is per-axis per-step velocity damping, not a surface
|
|
414
557
|
coefficient — see the built-in quirks below.
|
|
415
558
|
|
|
416
|
-
|
|
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
|
|
417
573
|
back onto `renderable.pos` after every step, so a direct write is simply erased
|
|
418
574
|
— no error. The built-in world integrates `pos` in place instead, so a write
|
|
419
575
|
survives but fights collision resolution and jitters. To teleport, go through
|
|
@@ -447,24 +603,36 @@ interface Contacts {
|
|
|
447
603
|
onCollisionStart?: (response: CollisionResponse, other: Renderable) => void;
|
|
448
604
|
}
|
|
449
605
|
(rock as Contacts).onCollisionStart = (response, other) => { … };
|
|
450
|
-
```
|
|
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:
|
|
451
617
|
|
|
452
618
|
> On the built-in world, `onCollision` fires **twice per frame** for
|
|
453
619
|
> dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT
|
|
454
620
|
> response with a fixed `a`/`b` per pair. On the adapters it is routed to their
|
|
455
|
-
> "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`.
|
|
456
624
|
|
|
457
625
|
The legacy dedupe idiom is `if (this !== response.a) return;`. Defining
|
|
458
626
|
`onCollisionActive` suppresses the legacy callback on that renderable (the
|
|
459
627
|
"supersedes" rule, applied per side so `a` and `b` can migrate independently),
|
|
460
|
-
|
|
628
|
+
on every adapter.
|
|
461
629
|
|
|
462
630
|
**Return values only work on the legacy handler.** `return false` from
|
|
463
|
-
`onCollision` skips the built-in SAT push-out;
|
|
464
|
-
`onCollisionActive` return is discarded.
|
|
465
|
-
response, set `bodyDef.isSensor` /
|
|
466
|
-
trigger. With no `onCollision`
|
|
467
|
-
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.
|
|
468
636
|
|
|
469
637
|
The two handler families do **not** get the same response object:
|
|
470
638
|
|
|
@@ -475,15 +643,22 @@ The two handler families do **not** get the same response object:
|
|
|
475
643
|
| contact axis | `response.overlapN` / `overlapV` | `response.normal` |
|
|
476
644
|
| also carries | `aInB`, `bInA`, `indexShapeA/B`, `isTriggerContact` | `pair` (adapter-native, absent on built-in) |
|
|
477
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
|
+
|
|
478
650
|
On the modern response `overlap` / `overlapN` / `overlapV` are still present but
|
|
479
|
-
`@deprecated` (and `undefined` under the
|
|
480
|
-
`normal`. `
|
|
481
|
-
|
|
482
|
-
|
|
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`.
|
|
483
656
|
|
|
484
657
|
One portability wrinkle: the built-in adapter dispatches `onCollisionEnd` with
|
|
485
|
-
`undefined` as the response; the
|
|
486
|
-
|
|
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.
|
|
487
662
|
|
|
488
663
|
## Never mutate the world during contact dispatch
|
|
489
664
|
|
|
@@ -508,13 +683,19 @@ What actually happens per adapter:
|
|
|
508
683
|
planck's `begin-contact` / `end-contact` events, which fire *while the world
|
|
509
684
|
is locked*. Adding a renderable with a `bodyDef` there makes planck's
|
|
510
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.
|
|
511
689
|
`onCollisionActive` is dispatched after the step and is the safe one.
|
|
512
690
|
- **matter** — callbacks come from matter's `collisionStart` / `collisionActive`
|
|
513
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`.
|
|
514
695
|
|
|
515
696
|
## Spatial queries
|
|
516
697
|
|
|
517
|
-
`raycast` and `queryAABB` are implemented by
|
|
698
|
+
`raycast` and `queryAABB` are implemented by every adapter:
|
|
518
699
|
|
|
519
700
|
```js
|
|
520
701
|
import { Rect, Vector2d } from "melonjs";
|
|
@@ -529,20 +710,42 @@ const inArea = app.world.adapter.queryAABB(new Rect(x, y, w, h));
|
|
|
529
710
|
|
|
530
711
|
`querySphere(centre, radius)` (or `querySphere(sphere)`) is **built-in only**,
|
|
531
712
|
and only active under a 3D broadphase — i.e. `world.sortOn === "depth"`, which
|
|
532
|
-
`Camera3d` sets. So is `raycast3d`.
|
|
533
|
-
|
|
534
|
-
|
|
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.
|
|
726
|
+
|
|
727
|
+
On the built-in adapter a 2D ray measures each shape the way the narrowphase
|
|
728
|
+
measures it against a planar shape: polygons, `Rect`, `RoundRect` and `Line`
|
|
729
|
+
by their edges, an `Ellipse` with its rotation, a `Box3d` by its XY footprint
|
|
730
|
+
and a `Sphere` as a circle of its radius (a 2D ray is unbounded along Z). A
|
|
731
|
+
`Point` has no area to cross and is skipped, as is an unknown shape type,
|
|
732
|
+
with a one-time console warning. Before 20.13 a body carrying a `Point`,
|
|
733
|
+
`Sphere` or `Box3d` made every ray that reached it throw, and a rotated
|
|
734
|
+
ellipse was hit where it would have been unrotated.
|
|
535
735
|
|
|
536
736
|
The legacy `collision.rayCast(line, result)` is a different API: it takes a
|
|
537
737
|
`Line`, returns an **array** of every intersecting renderable, and only exists
|
|
538
738
|
on the built-in path.
|
|
539
739
|
|
|
540
|
-
Two prerequisites that produce empty results rather
|
|
740
|
+
Two prerequisites on the built-in adapter that produce empty results rather
|
|
741
|
+
than errors:
|
|
541
742
|
|
|
542
743
|
- **`isKinematic` must be `false`** for a renderable to be in the broadphase at
|
|
543
744
|
all (a `Body` sets this for you).
|
|
544
745
|
- On the **built-in** adapter, the broadphase is cleared and rebuilt inside
|
|
545
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.
|
|
546
749
|
|
|
547
750
|
## 3D collision: `Box3d`, `Sphere` and the Z pushback
|
|
548
751
|
|
|
@@ -553,10 +756,11 @@ along **Z** as well as X and Y.
|
|
|
553
756
|
```js
|
|
554
757
|
import { Box3d, Sphere } from "melonjs";
|
|
555
758
|
|
|
556
|
-
// a box: centre
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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));
|
|
560
764
|
```
|
|
561
765
|
|
|
562
766
|
Reach for these for anything moving in the XZ plane under a `Camera3d` — a
|
|
@@ -600,7 +804,7 @@ the mistake to avoid: correct for a box pair, and for a sphere it throws away
|
|
|
600
804
|
the planar half of the contact.
|
|
601
805
|
|
|
602
806
|
**Mixed pairs degrade to 2D.** A `Box3d` or a `Sphere` against a `Polygon`,
|
|
603
|
-
`
|
|
807
|
+
`Rect`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and
|
|
604
808
|
treats the planar shape as unbounded along Z, which is what keeps a sphere or
|
|
605
809
|
box body colliding with an ordinary Tiled collision layer. `overlapZ` is zero
|
|
606
810
|
for those, since only one side has a depth to resolve. Give BOTH sides a 3D
|
|
@@ -629,8 +833,9 @@ group.resize(width, height);
|
|
|
629
833
|
```
|
|
630
834
|
|
|
631
835
|
`enableChildBoundsUpdate` also gives it one, but it re-measures the whole child
|
|
632
|
-
list on every update
|
|
633
|
-
|
|
836
|
+
list on every update (after the children have moved, and before each culling
|
|
837
|
+
check while it is off screen) — reach for it only when the extent genuinely IS
|
|
838
|
+
the members' union and moves with them (a flock, a squad), not to give a fixed
|
|
634
839
|
group a size.
|
|
635
840
|
|
|
636
841
|
## Built-in world quirks
|
|
@@ -650,19 +855,35 @@ real rigid-body engine:
|
|
|
650
855
|
not contact state — it says nothing about what a body is touching.
|
|
651
856
|
- **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate
|
|
652
857
|
`app.world.adapter.gravity` at runtime, or pass
|
|
653
|
-
`physic:
|
|
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`
|
|
654
860
|
(default `1`) scales it per body; `0` disables gravity for that body and is
|
|
655
861
|
the portable replacement for the deprecated `body.ignoreGravity`.
|
|
656
862
|
- **`applyForce(x, y)` is linear only** unless you pass the optional
|
|
657
863
|
`(pointX, pointY)` application point, which generates a torque
|
|
658
864
|
`τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.
|
|
659
|
-
`applyTorque(τ)` is the direct form.
|
|
865
|
+
`applyTorque(τ)` is the direct form. The point is in **world** coordinates,
|
|
866
|
+
as on the other adapters, and the lever arm is measured from the centre of
|
|
867
|
+
the shapes where they collide: the renderable's drawn frame (so the anchor
|
|
868
|
+
counts) plus any parent container's position. `renderable.getBounds()` is
|
|
869
|
+
in that space, so `b.centerX, b.top` is the top centre.
|
|
660
870
|
- **Force accumulators reset at end-of-step** — for every body, including
|
|
661
871
|
static, paused and off-screen ones — so forces must be applied every frame to
|
|
662
872
|
persist.
|
|
663
|
-
- **`isGrounded`
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
880
|
+
by a ceiling while rising (or `jumping`) ends the jump and falls, and a
|
|
881
|
+
side contact, a body touched from above while standing, or a body with
|
|
882
|
+
`gravityScale` 0 leaves them alone. Before 20.13 a magnitude test decided
|
|
883
|
+
instead, so anything that sank a pixel or more per step — gravity above
|
|
884
|
+
about `1.02`, mass above about `1.03`, `timer.tick > 1` at a low frame
|
|
885
|
+
rate, a floor moving up under it — stayed `falling` while sitting flush on
|
|
886
|
+
the floor, and could never jump.
|
|
666
887
|
- **`def.density` maps 1:1 to `body.mass`** (default `1`), and `def.friction` is
|
|
667
888
|
**ignored** entirely. `body.friction` is a per-axis `Vector2d` of per-step
|
|
668
889
|
velocity damping fed from `def.frictionAir`, not a surface coefficient.
|
|
@@ -678,7 +899,7 @@ real rigid-body engine:
|
|
|
678
899
|
- **`addBody` throws on double-registration** for a renderable that is already
|
|
679
900
|
adapter-managed.
|
|
680
901
|
|
|
681
|
-
### Rotation collides on planck and
|
|
902
|
+
### Rotation collides on planck, matter and Rapier, but not on the built-in solver
|
|
682
903
|
|
|
683
904
|
This is the sharpest behaviour difference between the adapters, and it is
|
|
684
905
|
silent — nothing warns you.
|
|
@@ -686,14 +907,20 @@ silent — nothing warns you.
|
|
|
686
907
|
| adapter | a rotating body |
|
|
687
908
|
| --- | --- |
|
|
688
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 |
|
|
689
|
-
| **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 |
|
|
690
911
|
|
|
691
912
|
So a turret, a swinging platform or a tumbling crate that must *collide*
|
|
692
|
-
rotated needs
|
|
913
|
+
rotated needs a rigid-body adapter. Under the builtin solver, approximate it with a
|
|
693
914
|
shape that is close enough at every angle (a circle, or a square for
|
|
694
915
|
90° steps), or rotate the shapes yourself with `body.rotate(angle)` — which
|
|
695
916
|
does rotate them, unlike `setAngle()`.
|
|
696
917
|
|
|
918
|
+
The visual turn pivots on the centre of the body's shapes, where they are
|
|
919
|
+
drawn and collide, whatever the renderable's `anchorPoint`. Before 20.13 it
|
|
920
|
+
ignored the anchor, so a centre-anchored renderable turned about its
|
|
921
|
+
bottom-right corner and swung away from where it stood; only a `(0, 0)`
|
|
922
|
+
anchor turned in place.
|
|
923
|
+
|
|
697
924
|
`body.rotate()` applies a **delta** and does not update `body.angle`; the two
|
|
698
925
|
are unlinked, so a body driven by `rotate()` still reports an angle of `0`.
|
|
699
926
|
Track it yourself if you need to read it back. Before 20.7 it also threw on a
|
|
@@ -701,19 +928,20 @@ Track it yourself if you need to read it back. Before 20.7 it also threw on a
|
|
|
701
928
|
grown.
|
|
702
929
|
|
|
703
930
|
`adapter.getBodyShapes(renderable)` reports whichever is true: the geometry
|
|
704
|
-
the engine is actually simulating on planck and
|
|
705
|
-
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
|
|
706
933
|
there they genuinely are. That is what the debug overlay draws, so the hitbox
|
|
707
934
|
you see is the hitbox that collides — a sprite spinning inside a stationary red
|
|
708
935
|
box on the builtin is the solver telling you the truth, not a drawing bug.
|
|
709
936
|
|
|
710
|
-
On
|
|
937
|
+
On the adapters that report comes from the engine rather than from your
|
|
711
938
|
definitions, so it shows the approximations they make: an `Ellipse` appears as
|
|
712
939
|
the circle of average radius it is simulated as, a concave polygon as the
|
|
713
|
-
convex
|
|
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
|
|
714
942
|
authored shapes are untouched and still readable on `renderable.bodyDef.shapes`.
|
|
715
943
|
|
|
716
|
-
**If you need rotation to affect collision, use planck or
|
|
944
|
+
**If you need rotation to affect collision, use planck, matter or Rapier.** That is the
|
|
717
945
|
dividing line: the builtin is a position-based solver whose SAT never reads
|
|
718
946
|
`body.angle`, so rotation there is a visual property. `body.rotate(angle)` is
|
|
719
947
|
the workaround, not a rotating body — it permanently moves the shapes, does not
|
|
@@ -728,61 +956,140 @@ Same call, different magnitude — these are the ones that waste an afternoon:
|
|
|
728
956
|
|
|
729
957
|
- **The portable API converts for you, the native handle does not.**
|
|
730
958
|
`adapter.setVelocity` / `body.setVelocity(x, y)` are in pixels on every
|
|
731
|
-
adapter; reaching for the raw handle (`body.setLinearVelocity` on planck
|
|
732
|
-
you in metres per second, scaled by
|
|
733
|
-
|
|
734
|
-
|
|
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.
|
|
735
967
|
- **Force magnitudes are not comparable** between adapters; re-tune rather than
|
|
736
|
-
reuse. `applyForce` is
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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`.
|
|
740
977
|
- **`body.position` is the centroid in matter**, while `renderable.pos` is
|
|
741
|
-
|
|
742
|
-
stays correct — but read `body.position` directly and you
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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.
|
|
747
996
|
- **`isGrounded` is capability-gated** (`capabilities.isGrounded`) and computed
|
|
748
|
-
differently per adapter:
|
|
749
|
-
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.
|
|
750
1001
|
|
|
751
1002
|
Adapter-specific escapes exist (`(this.body as MatterAdapter.Body).frictionAir`,
|
|
752
1003
|
`adapter.matter.Constraint.create(...)`) but are not portable — flag them if you
|
|
753
1004
|
use them. `adapter.capabilities` (`constraints`,
|
|
754
1005
|
`continuousCollisionDetection`, `sleepingBodies`, `raycasts`, `raycasts3d`,
|
|
755
|
-
`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()`.
|
|
756
1030
|
|
|
757
1031
|
## Symptom → cause
|
|
758
1032
|
|
|
759
1033
|
| symptom | cause |
|
|
760
1034
|
|---|---|
|
|
761
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` |
|
|
762
1038
|
| collision handler runs twice per frame | legacy `onCollision` on a dynamic pair (built-in) — use `onCollisionActive` |
|
|
763
1039
|
| `return false` from a handler does nothing | return values are only honoured by legacy `onCollision` — use `isSensor` / `isTrigger` |
|
|
764
1040
|
| crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |
|
|
765
1041
|
| `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |
|
|
766
|
-
| `
|
|
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 |
|
|
767
1048
|
| a 3D contact reports no overlap to push against | read `overlapNZ` / `overlapZ` — a Z-axis MTV leaves `overlapN` / `overlapV` at zero |
|
|
768
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 |
|
|
769
|
-
| `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)` |
|
|
770
1057
|
| `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |
|
|
771
1058
|
| off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |
|
|
772
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 |
|
|
773
|
-
| 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
|
|
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 |
|
|
774
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 |
|
|
775
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 |
|
|
776
1063
|
| a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |
|
|
777
|
-
| 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
|
|
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 |
|
|
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 |
|
|
778
1066
|
| forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |
|
|
779
|
-
| `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid,
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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` |
|
|
780
1071
|
| `bodyDef.shapes: "…"` throws "is not loaded" | the name must match the `name` the JSON was preloaded under, not its filename |
|
|
781
1072
|
| everything falls far too fast or too slow after swapping backends | gravity is a different unit per backend; see the table above |
|
|
782
1073
|
| debris lands holding its spawn angle and never tumbles | `fixedRotation: true`, or matter-adapter < 1.4.0 / planck-adapter < 1.5.0 |
|
|
783
|
-
| a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use
|
|
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)` |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
1082
|
+
| a ray passes through a rotated ellipse, or stops short of it (built-in) | before 20.13 the raycast ignored `Ellipse#angle`; upgrade |
|
|
784
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 |
|
|
785
|
-
| a Tiled polyline becomes a solid box on matter | matter
|
|
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 |
|
|
786
1093
|
|
|
787
1094
|
## Related skills
|
|
788
1095
|
|