melonjs 20.2.0 → 20.3.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 +86 -8
- package/build/application/settings.d.ts +3 -2
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/audio.d.ts +2 -2
- package/build/audio/audio.d.ts.map +1 -1
- package/build/audio/backend/buffer.d.ts +26 -0
- package/build/audio/backend/buffer.d.ts.map +1 -0
- package/build/audio/backend/cache.d.ts +2 -0
- package/build/audio/backend/cache.d.ts.map +1 -0
- package/build/audio/backend/core.d.ts +24 -0
- package/build/audio/backend/core.d.ts.map +1 -0
- package/build/audio/backend/engine.d.ts +54 -0
- package/build/audio/backend/engine.d.ts.map +1 -0
- package/build/audio/backend/sound.d.ts +79 -0
- package/build/audio/backend/sound.d.ts.map +1 -0
- package/build/audio/backend/spatial.d.ts +191 -0
- package/build/audio/backend/spatial.d.ts.map +1 -0
- package/build/audio/backend/types.d.ts +97 -0
- package/build/audio/backend/types.d.ts.map +1 -0
- package/build/audio/backend/voice.d.ts +33 -0
- package/build/audio/backend/voice.d.ts.map +1 -0
- package/build/audio/playback.d.ts +49 -4
- package/build/audio/playback.d.ts.map +1 -1
- package/build/audio/{backend.d.ts → state.d.ts} +13 -11
- package/build/audio/state.d.ts.map +1 -0
- package/build/audio/types.d.ts +93 -1
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts +5 -5
- package/build/const.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8793 -10171
- package/build/index.js.map +4 -4
- package/build/input/input.d.ts +19 -0
- package/build/input/input.d.ts.map +1 -1
- package/build/lang/deprecated.d.ts +4 -2
- package/build/lang/deprecated.d.ts.map +1 -1
- package/build/level/gltf/GLTFScene.d.ts +12 -0
- package/build/level/gltf/GLTFScene.d.ts.map +1 -1
- package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
- package/build/loader/loader.d.ts +4 -40
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/parsers/audio.d.ts +31 -0
- package/build/loader/parsers/audio.d.ts.map +1 -0
- package/build/loader/parsers/shader.d.ts.map +1 -1
- package/build/physics/adapter.d.ts +3 -3
- package/build/physics/builtin/body.d.ts +23 -24
- package/build/physics/builtin/body.d.ts.map +1 -1
- package/build/physics/builtin/detector.d.ts +1 -1
- package/build/plugin/plugin.d.ts +9 -1
- package/build/plugin/plugin.d.ts.map +1 -1
- package/build/polyfill/index.d.ts +1 -4
- package/build/polyfill/index.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +16 -0
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts +16 -10
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite.d.ts +7 -7
- package/build/renderable/sprite.d.ts.map +1 -1
- package/build/state/stage.d.ts +4 -2
- package/build/state/stage.d.ts.map +1 -1
- package/build/system/event.d.ts +1 -1
- package/build/system/platform.d.ts +21 -1
- package/build/system/platform.d.ts.map +1 -1
- package/build/utils/fetchdata.d.ts.map +1 -0
- package/build/video/blendmodes.d.ts +1 -1
- package/build/video/canvas/canvas_renderer.d.ts +5 -2
- package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
- package/build/video/effects/blur.d.ts +4 -4
- package/build/video/effects/blur.d.ts.map +1 -1
- package/build/video/effects/chromaticAberration.d.ts +5 -5
- package/build/video/effects/chromaticAberration.d.ts.map +1 -1
- package/build/video/effects/colorMatrix.d.ts +5 -5
- package/build/video/effects/colorMatrix.d.ts.map +1 -1
- package/build/video/effects/desaturate.d.ts +5 -5
- package/build/video/effects/desaturate.d.ts.map +1 -1
- package/build/video/effects/dissolve.d.ts +4 -4
- package/build/video/effects/dissolve.d.ts.map +1 -1
- package/build/video/effects/dropShadow.d.ts +5 -5
- package/build/video/effects/dropShadow.d.ts.map +1 -1
- package/build/video/effects/flash.d.ts +4 -4
- package/build/video/effects/flash.d.ts.map +1 -1
- package/build/video/effects/glow.d.ts +5 -5
- package/build/video/effects/glow.d.ts.map +1 -1
- package/build/video/effects/hologram.d.ts +4 -4
- package/build/video/effects/hologram.d.ts.map +1 -1
- package/build/video/effects/invert.d.ts +5 -5
- package/build/video/effects/invert.d.ts.map +1 -1
- package/build/video/effects/outline.d.ts +6 -6
- package/build/video/effects/outline.d.ts.map +1 -1
- package/build/video/effects/pixelate.d.ts +4 -4
- package/build/video/effects/pixelate.d.ts.map +1 -1
- package/build/video/effects/radialGradient.d.ts +2 -2
- package/build/video/effects/scanline.d.ts +6 -6
- package/build/video/effects/scanline.d.ts.map +1 -1
- package/build/video/effects/sepia.d.ts +5 -5
- package/build/video/effects/sepia.d.ts.map +1 -1
- package/build/video/effects/shadereffect.d.ts +12 -10
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/effects/shine.d.ts +3 -3
- package/build/video/effects/shine.d.ts.map +1 -1
- package/build/video/effects/tintPulse.d.ts +4 -4
- package/build/video/effects/tintPulse.d.ts.map +1 -1
- package/build/video/effects/vignette.d.ts +6 -6
- package/build/video/effects/vignette.d.ts.map +1 -1
- package/build/video/effects/wave.d.ts +4 -4
- package/build/video/effects/wave.d.ts.map +1 -1
- package/build/video/texture/atlas.d.ts +1 -1
- package/build/video/webgl/glshader.d.ts +2 -2
- package/build/video/webgl/webgl_renderer.d.ts +2 -1
- package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +2 -1
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +5 -9
- package/skills/AGENTS.md +67 -0
- package/skills/melonjs/SKILL.md +156 -0
- package/skills/melonjs-20-migration/SKILL.md +160 -0
- package/skills/melonjs-3d/SKILL.md +226 -0
- package/skills/melonjs-3d-assets/SKILL.md +239 -0
- package/skills/melonjs-audio/SKILL.md +215 -0
- package/skills/melonjs-camera-and-drawing/SKILL.md +171 -0
- package/skills/melonjs-deployment/SKILL.md +113 -0
- package/skills/melonjs-effects-and-shaders/SKILL.md +177 -0
- package/skills/melonjs-events/SKILL.md +159 -0
- package/skills/melonjs-getting-started/SKILL.md +183 -0
- package/skills/melonjs-input/SKILL.md +166 -0
- package/skills/melonjs-lighting/SKILL.md +139 -0
- package/skills/melonjs-loading-assets/SKILL.md +157 -0
- package/skills/melonjs-particles-and-trails/SKILL.md +136 -0
- package/skills/melonjs-performance/SKILL.md +178 -0
- package/skills/melonjs-physics/SKILL.md +321 -0
- package/skills/melonjs-plugins/SKILL.md +191 -0
- package/skills/melonjs-renderables/SKILL.md +214 -0
- package/skills/melonjs-renderer-backends/SKILL.md +173 -0
- package/skills/melonjs-scenes-and-state/SKILL.md +180 -0
- package/skills/melonjs-sprites-and-animation/SKILL.md +173 -0
- package/skills/melonjs-tilemaps/SKILL.md +193 -0
- package/skills/melonjs-ui-and-text/SKILL.md +175 -0
- package/build/audio/backend.d.ts.map +0 -1
- package/build/loader/parsers/fetchdata.d.ts.map +0 -1
- /package/build/{loader/parsers → utils}/fetchdata.d.ts +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-loading-assets
|
|
3
|
+
description: "Use this skill for loading and managing game assets in melonJS — the resource descriptor format, the full asset type list, preload versus load, retrieving loaded assets, base URLs and cross-origin settings, and the loading screen. Triggers on: loader, loader.preload, loader.load, resources, getImage, getJSON, getTMX, getGLTF, getVideo, getFont, setBaseURL, setOptions, crossOrigin, asset, preload, LOADER_COMPLETE, loading screen, unload."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Loading assets
|
|
8
|
+
|
|
9
|
+
## The resource descriptor
|
|
10
|
+
|
|
11
|
+
Every asset is `{ name, type, src }` plus type-specific extras:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { loader } from "melonjs";
|
|
15
|
+
|
|
16
|
+
loader.setOptions({ crossOrigin: "anonymous" }); // needed for remote assets
|
|
17
|
+
|
|
18
|
+
await loader.preload([
|
|
19
|
+
{ name: "tiles", type: "image", src: "data/img/tiles.png" },
|
|
20
|
+
{ name: "map1", type: "tmx", src: "data/map/map1.tmx" },
|
|
21
|
+
{ name: "tileset", type: "tsx", src: "data/map/tileset.tsx" },
|
|
22
|
+
{ name: "sfx", type: "audio", src: "data/sfx/" },
|
|
23
|
+
{ name: "font", type: "fontface", src: "data/font/PressStart2P.ttf" },
|
|
24
|
+
{ name: "atlas", type: "json", src: "data/img/atlas.json" },
|
|
25
|
+
]);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`preload()` has been awaitable since 19.8. The callback form still works and
|
|
29
|
+
takes a third argument to suppress the built-in loading screen:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
loader.preload(resources, onLoaded, /* switchToLoadState */ false);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The type list
|
|
36
|
+
|
|
37
|
+
The type strings are melonJS-specific — this is not a generic loader:
|
|
38
|
+
|
|
39
|
+
| type | for |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `"image"` | textures, sprite sheets, normal maps |
|
|
42
|
+
| `"json"` | data, texture-atlas descriptors |
|
|
43
|
+
| `"tmx"` / `"tsx"` | Tiled maps and external tilesets |
|
|
44
|
+
| `"audio"` | sound — **`src` is a directory**, see below |
|
|
45
|
+
| `"video"` | video textures |
|
|
46
|
+
| `"binary"` | raw data, bitmap font `.fnt` descriptors |
|
|
47
|
+
| `"fontface"` | web fonts (`.ttf`, `.woff`) |
|
|
48
|
+
| `"aseprite"` | Aseprite JSON + image |
|
|
49
|
+
| `"shader"` | GLSL/WGSL shader sources |
|
|
50
|
+
| `"obj"` / `"mtl"` | Wavefront models and materials |
|
|
51
|
+
| `"gltf"` / `"glb"` | glTF scenes |
|
|
52
|
+
| `"js"` | scripts |
|
|
53
|
+
|
|
54
|
+
## Two conventions that produce 404s
|
|
55
|
+
|
|
56
|
+
**Audio `src` is a directory.** The loader appends the asset name plus each
|
|
57
|
+
format from `audio.init()`, and picks the first that decodes:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
audio.init("mp3,ogg"); // before preloading audio
|
|
61
|
+
{ name: "cling", type: "audio", src: "data/sfx/" } // → data/sfx/cling.mp3
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A full filename here gives you a 404. And `audio.init()` must run **before** any
|
|
65
|
+
audio asset is preloaded.
|
|
66
|
+
|
|
67
|
+
**A Tiled map needs its dependencies listed too** — the `.tmx`, any external
|
|
68
|
+
`.tsx`, and the tileset images. Listing only the map produces a blank level.
|
|
69
|
+
|
|
70
|
+
## Retrieving loaded assets
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
loader.getImage("tiles");
|
|
74
|
+
loader.getJSON("atlas");
|
|
75
|
+
loader.getTMX("map1");
|
|
76
|
+
loader.getGLTF("diorama");
|
|
77
|
+
loader.getVideo("intro");
|
|
78
|
+
loader.getFont("font");
|
|
79
|
+
loader.getBinary("font-fnt");
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Most engine classes take the asset **name** directly, so you often do not need
|
|
83
|
+
these — `new Sprite(x, y, { image: "tiles" })` resolves it for you.
|
|
84
|
+
|
|
85
|
+
## Base URLs
|
|
86
|
+
|
|
87
|
+
Set a prefix per asset type rather than repeating it:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
loader.setBaseURL("image", "data/img/");
|
|
91
|
+
loader.setBaseURL("audio", "data/sfx/");
|
|
92
|
+
loader.setBaseURL("*", "https://cdn.example.com/"); // every 2D type at once
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `"*"` wildcard deliberately **skips `obj`, `mtl`, `gltf` and `glb`**: those
|
|
96
|
+
resolve their own internal references (a `map_Kd` texture, a glTF `.bin`)
|
|
97
|
+
relative to their file, so a global prefix would double-prefix them. Set those
|
|
98
|
+
types individually.
|
|
99
|
+
|
|
100
|
+
## Progress and completion
|
|
101
|
+
|
|
102
|
+
Use the events. The old callback properties (`loader.onload`, `onProgress`,
|
|
103
|
+
`onError`) were deprecated in 18.2 and **removed in 20.3** — being `let`
|
|
104
|
+
bindings on an ES module namespace they could never be assigned from outside in
|
|
105
|
+
the first place, so `loader.onProgress = fn` always threw a `TypeError`:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import { event } from "melonjs";
|
|
109
|
+
|
|
110
|
+
event.on(event.LOADER_PROGRESS, (progress) => { /* 0..1 */ });
|
|
111
|
+
event.on(event.LOADER_COMPLETE, () => { /* … */ });
|
|
112
|
+
event.on(event.LOADER_ERROR, (res) => { /* … */ });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Unloading
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
loader.unload({ name: "map1", type: "tmx" });
|
|
119
|
+
loader.unloadAll();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`unloadAll()` also releases audio and GPU resources such as shader programs, so
|
|
123
|
+
it is the right teardown for switching between large levels.
|
|
124
|
+
|
|
125
|
+
## Loading between scenes
|
|
126
|
+
|
|
127
|
+
The default loading screen appears automatically when `preload` switches state.
|
|
128
|
+
For a custom one, register a `Stage` against `state.LOADING`:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
state.set(state.LOADING, new MyLoadingScreen());
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
If you preload without a loading stage and then change state immediately, pass
|
|
135
|
+
`forceChange`:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
state.change(state.PLAY, true);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Symptom → cause
|
|
142
|
+
|
|
143
|
+
| symptom | cause |
|
|
144
|
+
|---|---|
|
|
145
|
+
| audio 404s | `src` given as a file instead of a directory |
|
|
146
|
+
| audio fails, `preload()` rejects with "Failed loading resource" | `audio.init()` not called before preloading — the real message is replaced by the loader's |
|
|
147
|
+
| blank level, missing tiles | `.tsx` or tileset image not listed in the resources |
|
|
148
|
+
| cross-origin textures fail | missing `loader.setOptions({ crossOrigin: "anonymous" })` |
|
|
149
|
+
| text renders in a fallback font | web font not preloaded as `"fontface"` |
|
|
150
|
+
| `BitmapText` renders nothing | the `.fnt` (as `"binary"`) or its image is missing |
|
|
151
|
+
| `TypeError` assigning `loader.onProgress` / `onload` | removed in 20.3; they were never writable — use the `LOADER_*` events |
|
|
152
|
+
|
|
153
|
+
## Related skills
|
|
154
|
+
|
|
155
|
+
- `melonjs-getting-started` — where preloading sits in the bootstrap
|
|
156
|
+
- `melonjs-tilemaps` — the full Tiled asset set
|
|
157
|
+
- `melonjs-audio` — formats and the directory convention
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-particles-and-trails
|
|
3
|
+
description: "Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: ParticleEmitter, particles, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, Trail, explosion, smoke, sparks, exhaust, emitter."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Particles and trails
|
|
8
|
+
|
|
9
|
+
## An emitter does nothing until you tell it to emit
|
|
10
|
+
|
|
11
|
+
Adding a `ParticleEmitter` to the world is not enough. Nothing appears, and
|
|
12
|
+
nothing warns you.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
const emitter = new ParticleEmitter(x, y, {
|
|
16
|
+
image: loader.getImage("spark"),
|
|
17
|
+
totalParticles: 60,
|
|
18
|
+
speed: 4,
|
|
19
|
+
minLife: 200, maxLife: 800,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
world.addChild(emitter);
|
|
23
|
+
emitter.streamParticles(); // continuous — or:
|
|
24
|
+
emitter.burstParticles(); // one shot
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`streamParticles(ms)` takes an optional run time; without one it uses the
|
|
28
|
+
`duration` setting, which defaults to `Infinity` — a stream runs until
|
|
29
|
+
`stopStream()`. `burstParticles(n)` launches `n`, or `totalParticles`.
|
|
30
|
+
|
|
31
|
+
For one-shot effects, `autoDestroyOnComplete: true` removes the emitter once its
|
|
32
|
+
particles have finished. The `onComplete` callback fires at that same moment
|
|
33
|
+
either way.
|
|
34
|
+
|
|
35
|
+
For repeated effects (muzzle flashes, impacts) keep **one** long-lived emitter
|
|
36
|
+
and re-aim it per shot, rather than allocating an emitter per event.
|
|
37
|
+
|
|
38
|
+
## `referenceSpace` — which frame particles live in
|
|
39
|
+
|
|
40
|
+
Added in 20.2. This decides what a particle's position is measured *from*, and
|
|
41
|
+
it is the difference between an effect that trails behind a moving object and
|
|
42
|
+
one that drags along with it.
|
|
43
|
+
|
|
44
|
+
| value | particles are measured from | use for |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `"local"` (default) | the emitter | flames, auras, anything welded to the object |
|
|
47
|
+
| `"world"` | the container the emitter sits in | **trails, smoke, exhaust, dust** |
|
|
48
|
+
| a `Container` | that container | a moving frame — snow inside a moving carriage |
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// exhaust that stays where it was emitted, so the ship flies away from it
|
|
52
|
+
const exhaust = new ParticleEmitter(x, y, { referenceSpace: "world", /* … */ });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With the default `"local"`, a moving emitter carries its whole cloud along —
|
|
56
|
+
which is right for a torch flame and wrong for a smoke trail. Changing it at
|
|
57
|
+
runtime re-bases live particles, so nothing jumps.
|
|
58
|
+
|
|
59
|
+
## Particle tuning changed in 20.2
|
|
60
|
+
|
|
61
|
+
The 20.2 particle transform fix corrected a drift where drawn positions ran past
|
|
62
|
+
simulated ones. Particles now travel roughly **half as far** over their life as
|
|
63
|
+
they did before.
|
|
64
|
+
|
|
65
|
+
Consequence: **speed values tuned on 19.x or earlier look wrong on 20.2+.** If
|
|
66
|
+
you are copying emitter settings from older code or older tutorials, expect to
|
|
67
|
+
raise `speed` or `maxLife`.
|
|
68
|
+
|
|
69
|
+
## Trails
|
|
70
|
+
|
|
71
|
+
`Trail` is a ribbon that follows a target — use it rather than hand-rolling a
|
|
72
|
+
position history. **Its constructor takes one argument, the options object** —
|
|
73
|
+
no `x, y` pair like every other renderable:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const trail = new Trail({
|
|
77
|
+
target: sprite, // a renderable or a plain Vector2d
|
|
78
|
+
length: 24, // max points kept (default 20)
|
|
79
|
+
width: 20, // max ribbon width in px (default 10)
|
|
80
|
+
lifetime: 500, // per-point lifetime in ms (default 500)
|
|
81
|
+
widthCurve: [1, 0], // 0 = head, 1 = tail
|
|
82
|
+
gradient: ["#fff", "#f80", "#f000"],
|
|
83
|
+
});
|
|
84
|
+
trail.blendMode = "additive";
|
|
85
|
+
world.addChild(trail, 5);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Omit `target` and feed it yourself with `trail.addPoint(x, y)` — the mode for a
|
|
89
|
+
sword slash, where the ribbon follows a weapon tip rather than an object. Points
|
|
90
|
+
closer together than `minDistance` (default 4 px) are dropped.
|
|
91
|
+
|
|
92
|
+
**`Trail` is 2D.** `addPoint` takes `(x, y)`, points are stored without a `z`,
|
|
93
|
+
and the ribbon is drawn as connected screen-space quads. Under a `Camera3d` it
|
|
94
|
+
draws a flat ribbon that does not recede with perspective — so it is the wrong
|
|
95
|
+
tool for a trail behind an object moving *into* the scene. For that:
|
|
96
|
+
|
|
97
|
+
- **`ParticleEmitter` with `referenceSpace: "world"`** — particles stay where
|
|
98
|
+
they were dropped while the emitter moves on, and the emitter propagates its
|
|
99
|
+
depth to each particle so `Camera3d` projects them correctly. This is the
|
|
100
|
+
built-in answer for exhaust, ski tracks and speed trails in 3D.
|
|
101
|
+
- **A procedural `Mesh`** built from the object's recent path, with
|
|
102
|
+
`mesh.vertexColors` for a per-vertex gradient — more work, but a continuous
|
|
103
|
+
ribbon rather than discrete puffs.
|
|
104
|
+
|
|
105
|
+
## Performance
|
|
106
|
+
|
|
107
|
+
- Particles come from a shared pool, so a steady-state emitter allocates
|
|
108
|
+
nothing. Each emitter is still a `Container` that updates and draws every
|
|
109
|
+
frame, so one busy emitter beats many idle ones.
|
|
110
|
+
- `totalParticles` (default 50) is a cap on how many are alive at once in stream
|
|
111
|
+
mode; `burstParticles()` launches that many in one go, or the count you pass.
|
|
112
|
+
- `framesToSkip` (default 0) skips n updates between simulation steps — the
|
|
113
|
+
cheapest lever on an emitter with many particles.
|
|
114
|
+
- Blending is per particle, set through the `blendMode` setting or by assigning
|
|
115
|
+
`emitter.blendMode` (which reaches the particles already alive as well as
|
|
116
|
+
later ones). Additive is cheap everywhere: fixed-function on both GPU
|
|
117
|
+
backends, native `lighter` on Canvas. The expensive modes are the advanced CSS
|
|
118
|
+
ones (`overlay`, `soft-light`, `color-dodge`, `darken`, …), which on WebGL 2
|
|
119
|
+
and WebGPU cost a destination capture plus a shader composite *per blended
|
|
120
|
+
draw*.
|
|
121
|
+
|
|
122
|
+
## Symptom → cause
|
|
123
|
+
|
|
124
|
+
| symptom | cause |
|
|
125
|
+
|---|---|
|
|
126
|
+
| trail looks flat under a `Camera3d` | `Trail` is 2D — use a world-space emitter or a ribbon mesh |
|
|
127
|
+
| emitter added but nothing appears | `streamParticles()` / `burstParticles()` never called |
|
|
128
|
+
| the cloud follows a moving emitter | default `referenceSpace: "local"` — use `"world"` for trails |
|
|
129
|
+
| particles do not travel far enough | settings tuned pre-20.2, when drawn travel was doubled |
|
|
130
|
+
| one-shot emitters accumulate | missing `autoDestroyOnComplete: true` |
|
|
131
|
+
| frame rate drops with many effects | an emitter per event instead of one re-aimed emitter |
|
|
132
|
+
|
|
133
|
+
## Related skills
|
|
134
|
+
|
|
135
|
+
- `melonjs-renderables` — draw order and blend modes
|
|
136
|
+
- `melonjs-effects-and-shaders` — blend mode cost on each backend
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-performance
|
|
3
|
+
description: "Use this skill when a melonJS game drops frames, allocates heavily, or needs to scale to many objects. Covers object pooling, draw-call batching, atlases, baking with CanvasRenderTarget, instancing, culling and alwaysUpdate, the debug plugin, and where the real costs are. Triggers on: performance, slow, frame rate, fps, lag, pool, pool.register, pool.pull, batching, draw calls, InstancedMesh, culling, alwaysUpdate, inViewport, CanvasRenderTarget, optimise, profiling, debug plugin, memory."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Performance
|
|
8
|
+
|
|
9
|
+
## Measure first
|
|
10
|
+
|
|
11
|
+
Register the debug plugin and read the numbers before changing anything:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { plugin } from "melonjs";
|
|
15
|
+
import { DebugPanelPlugin } from "@melonjs/debug-plugin";
|
|
16
|
+
|
|
17
|
+
await app.init();
|
|
18
|
+
plugin.register(DebugPanelPlugin, "debugPanel");
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It reports frame time, draw calls, object counts, and can overlay collision
|
|
22
|
+
shapes and bounds. **Draw calls** is usually the number that matters.
|
|
23
|
+
|
|
24
|
+
## Batching: the biggest lever
|
|
25
|
+
|
|
26
|
+
Both GPU backends batch **across textures**, not just within one: the quad
|
|
27
|
+
batcher assigns each source image a texture unit and embeds the unit in the
|
|
28
|
+
vertex data, so several different images still flush as a single draw. The pool
|
|
29
|
+
is finite — the WebGL batcher gets `renderer.maxTextures` units (the
|
|
30
|
+
`maxTextures` application setting, `"auto"` = the device limit capped at 32), the
|
|
31
|
+
WebGPU batcher eight per segment. Overflow the pool and the batch flushes.
|
|
32
|
+
|
|
33
|
+
So the goal is not "one image" but "few state changes". What actually ends a
|
|
34
|
+
batch:
|
|
35
|
+
|
|
36
|
+
- **Overflowing the texture-unit pool** — pack sprites into a `TextureAtlas` and
|
|
37
|
+
prefer one large tileset over several small ones, so a screenful of sprites
|
|
38
|
+
fits inside the pool with room to spare.
|
|
39
|
+
- **Changing `blendMode`** — `setBlendMode` flushes before it touches GL state.
|
|
40
|
+
Group by blend mode where you can.
|
|
41
|
+
- **Switching batcher** — interleaving primitives (`strokeRect`, `fillRect`,
|
|
42
|
+
debug shapes) or meshes between sprites flushes the quad batcher and rebinds.
|
|
43
|
+
Draw all the sprites, then all the primitives.
|
|
44
|
+
- **Binding a custom shader** — a `ShaderEffect` on the quad batcher also turns
|
|
45
|
+
multi-texture batching *off* for the duration, dropping you back to one
|
|
46
|
+
texture per draw.
|
|
47
|
+
|
|
48
|
+
The advanced blend modes are the expensive ones: `overlay`, `difference`,
|
|
49
|
+
`hard-light`, `soft-light`, `color-dodge`, `color-burn`, `darken` and `lighten`
|
|
50
|
+
cannot be expressed as fixed-function blending, so each such draw is bracketed
|
|
51
|
+
on its own — capture the destination, redraw offscreen, composite through a
|
|
52
|
+
shader. Right for accents, wrong for hundreds of objects. `normal`, `additive`,
|
|
53
|
+
`multiply`, `screen` and `exclusion` are plain fixed-function state and cost
|
|
54
|
+
nothing extra.
|
|
55
|
+
|
|
56
|
+
## Pooling
|
|
57
|
+
|
|
58
|
+
Anything spawned frequently — bullets, particles, pickups, damage numbers —
|
|
59
|
+
should be pooled rather than allocated:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
pool.register("bullet", Bullet, true); // third arg = enable recycling
|
|
63
|
+
const b = pool.pull("bullet", x, y); // recycled: onResetEvent(x, y)
|
|
64
|
+
// fresh: new Bullet(x, y)
|
|
65
|
+
world.removeChild(b); // returns to the pool automatically
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`register` also publishes the class as a Tiled object factory under that name,
|
|
69
|
+
so an object with a matching class or name in a `.tmx` map instantiates it. Set
|
|
70
|
+
`pool.autoRegisterTiled = false` around the call for classes that should stay
|
|
71
|
+
programmatic.
|
|
72
|
+
|
|
73
|
+
Two rules that cause subtle bugs when missed:
|
|
74
|
+
|
|
75
|
+
- **`onResetEvent` must restore everything the object's lifetime mutates** —
|
|
76
|
+
alpha, tint, scale, animation, velocity. Anything you forget carries into the
|
|
77
|
+
next use, which looks like a random visual glitch.
|
|
78
|
+
- **Pooled objects do not fire `onDestroyEvent` on removal.** Removal always
|
|
79
|
+
calls `onDeactivateEvent`, then tries `pool.push`; only when that *fails* does
|
|
80
|
+
it fall through to `destroy()`, which is what calls `onDestroyEvent`. So a
|
|
81
|
+
successfully recycled object never sees it. Pair event subscriptions with
|
|
82
|
+
`onActivateEvent` / `onDeactivateEvent` instead, or you leak handlers.
|
|
83
|
+
|
|
84
|
+
Engine classes are poolable too: `pool.pull("me.Tween", target)`.
|
|
85
|
+
|
|
86
|
+
## `scale()` is multiplicative
|
|
87
|
+
|
|
88
|
+
A pooled sprite arrives carrying its previous life's transform, and calling
|
|
89
|
+
`scale()` each frame compounds. For absolute scaling:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
sprite.currentTransform.identity();
|
|
93
|
+
sprite.currentTransform.scale(s, s, 1);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Culling and update cost
|
|
97
|
+
|
|
98
|
+
- `Container` recomputes `inViewport` for every child each frame against every
|
|
99
|
+
active camera, and skips both its `draw()` **and** its `update(dt)` when it is
|
|
100
|
+
off-screen. That gate is `inViewport || alwaysUpdate`, and it applies to every
|
|
101
|
+
container in every game, whichever physics you use.
|
|
102
|
+
- On top of that, the **built-in physics adapter** gates body integration and
|
|
103
|
+
narrow-phase collision on the same `inViewport || alwaysUpdate` — so an
|
|
104
|
+
off-screen body stops simulating even if something else calls its `update`.
|
|
105
|
+
The planck and matter adapters have no such gate: they step the whole world
|
|
106
|
+
regardless of the camera.
|
|
107
|
+
- Set `alwaysUpdate = true` on anything that must keep moving off-screen, and
|
|
108
|
+
accept the cost. On many objects it removes the saving entirely.
|
|
109
|
+
- `floating = true` does **not** disable culling — it moves the test into screen
|
|
110
|
+
space: a floating renderable is checked against the camera's screen rectangle
|
|
111
|
+
(`0, 0, camera.width, camera.height`) instead of the world view. That is why a
|
|
112
|
+
HUD stays put as the camera moves. `Container.draw` then draws floating
|
|
113
|
+
children unconditionally, but `update()` is still gated, so a floating object
|
|
114
|
+
parked outside the screen rectangle stops updating.
|
|
115
|
+
|
|
116
|
+
## Bake once, draw many
|
|
117
|
+
|
|
118
|
+
When you would otherwise issue hundreds of identical draws every frame, render
|
|
119
|
+
once into a `CanvasRenderTarget` and draw the result as a single sprite. This is
|
|
120
|
+
the sanctioned idiom before reaching for a custom shader — hundreds of draws
|
|
121
|
+
collapse to one.
|
|
122
|
+
|
|
123
|
+
## Instancing in 3D
|
|
124
|
+
|
|
125
|
+
`InstancedMesh` draws one mesh many times in a single call — the difference
|
|
126
|
+
between a hundred trees and a hundred thousand. glTF scenes using
|
|
127
|
+
`EXT_mesh_gpu_instancing` load as an `InstancedMesh` automatically.
|
|
128
|
+
|
|
129
|
+
## Update loops
|
|
130
|
+
|
|
131
|
+
- `update(dt)` should **return `true` only when something changed**. Returning
|
|
132
|
+
`true` unconditionally forces redraw work every frame.
|
|
133
|
+
- Use `dt` for motion rather than assuming a fixed step — the engine already
|
|
134
|
+
paces it, and a fixed assumption breaks on high-refresh displays.
|
|
135
|
+
- Use `timer.setTimeout` / `setInterval`, which are pause-aware. `window` timers
|
|
136
|
+
keep firing behind a pause menu.
|
|
137
|
+
|
|
138
|
+
## Audio and assets
|
|
139
|
+
|
|
140
|
+
- `stream: true` on long music tracks plays through a streaming element instead
|
|
141
|
+
of decoding the whole file into memory.
|
|
142
|
+
- `pool` on an audio clip sizes the reuse pool for finished instances — it is
|
|
143
|
+
**not** a concurrency cap.
|
|
144
|
+
- `loader.unloadAll()` between large levels drops every loaded asset and
|
|
145
|
+
`destroy()`s the shader programs the loader owns — that last part is a real
|
|
146
|
+
GPU release. Images, by contrast, only lose their loader-side reference; the
|
|
147
|
+
renderer's texture cache is not purged by unloading.
|
|
148
|
+
|
|
149
|
+
## Where the costs actually are
|
|
150
|
+
|
|
151
|
+
In rough order for a typical 2D game:
|
|
152
|
+
|
|
153
|
+
1. Draw calls — fix with atlases and fewer tilesets
|
|
154
|
+
2. Overdraw from large blended sprites
|
|
155
|
+
3. Per-frame allocation — fix with pooling
|
|
156
|
+
4. `alwaysUpdate` on objects that do not need it
|
|
157
|
+
5. The engine itself, which is rarely the problem
|
|
158
|
+
|
|
159
|
+
## Symptom → cause
|
|
160
|
+
|
|
161
|
+
| symptom | cause |
|
|
162
|
+
|---|---|
|
|
163
|
+
| high draw-call count | the texture-unit pool overflows, or primitives/meshes are interleaved with sprites |
|
|
164
|
+
| draw calls jump when one effect is enabled | a custom shader on the quad batcher disables multi-texture batching |
|
|
165
|
+
| frame time spikes and GC pauses | allocating per frame instead of pooling |
|
|
166
|
+
| a recycled object looks wrong | `onResetEvent` does not restore every mutated property |
|
|
167
|
+
| sprite grows every frame | `scale()` is multiplicative — reset the transform |
|
|
168
|
+
| off-screen enemies stop moving | `update()` and built-in physics both gate on `inViewport`; set `alwaysUpdate` |
|
|
169
|
+
| a HUD element vanishes | `floating` is culled against the screen rect, and it sits outside it |
|
|
170
|
+
| everything updates even when idle | `update()` returning `true` unconditionally |
|
|
171
|
+
| memory grows with long music | missing `stream: true` |
|
|
172
|
+
| slow only on some machines | Canvas fallback — no GPU tilemap path |
|
|
173
|
+
|
|
174
|
+
## Related skills
|
|
175
|
+
|
|
176
|
+
- `melonjs-sprites-and-animation` — atlases and pooling in detail
|
|
177
|
+
- `melonjs-renderer-backends` — why performance differs per machine
|
|
178
|
+
- `melonjs-3d` — `InstancedMesh`
|