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.
Files changed (140) hide show
  1. package/README.md +86 -8
  2. package/build/application/settings.d.ts +3 -2
  3. package/build/application/settings.d.ts.map +1 -1
  4. package/build/audio/audio.d.ts +2 -2
  5. package/build/audio/audio.d.ts.map +1 -1
  6. package/build/audio/backend/buffer.d.ts +26 -0
  7. package/build/audio/backend/buffer.d.ts.map +1 -0
  8. package/build/audio/backend/cache.d.ts +2 -0
  9. package/build/audio/backend/cache.d.ts.map +1 -0
  10. package/build/audio/backend/core.d.ts +24 -0
  11. package/build/audio/backend/core.d.ts.map +1 -0
  12. package/build/audio/backend/engine.d.ts +54 -0
  13. package/build/audio/backend/engine.d.ts.map +1 -0
  14. package/build/audio/backend/sound.d.ts +79 -0
  15. package/build/audio/backend/sound.d.ts.map +1 -0
  16. package/build/audio/backend/spatial.d.ts +191 -0
  17. package/build/audio/backend/spatial.d.ts.map +1 -0
  18. package/build/audio/backend/types.d.ts +97 -0
  19. package/build/audio/backend/types.d.ts.map +1 -0
  20. package/build/audio/backend/voice.d.ts +33 -0
  21. package/build/audio/backend/voice.d.ts.map +1 -0
  22. package/build/audio/playback.d.ts +49 -4
  23. package/build/audio/playback.d.ts.map +1 -1
  24. package/build/audio/{backend.d.ts → state.d.ts} +13 -11
  25. package/build/audio/state.d.ts.map +1 -0
  26. package/build/audio/types.d.ts +93 -1
  27. package/build/audio/types.d.ts.map +1 -1
  28. package/build/camera/camera3d.d.ts +5 -5
  29. package/build/const.d.ts +2 -2
  30. package/build/index.d.ts.map +1 -1
  31. package/build/index.js +8793 -10171
  32. package/build/index.js.map +4 -4
  33. package/build/input/input.d.ts +19 -0
  34. package/build/input/input.d.ts.map +1 -1
  35. package/build/lang/deprecated.d.ts +4 -2
  36. package/build/lang/deprecated.d.ts.map +1 -1
  37. package/build/level/gltf/GLTFScene.d.ts +12 -0
  38. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  39. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  40. package/build/loader/loader.d.ts +4 -40
  41. package/build/loader/loader.d.ts.map +1 -1
  42. package/build/loader/parsers/audio.d.ts +31 -0
  43. package/build/loader/parsers/audio.d.ts.map +1 -0
  44. package/build/loader/parsers/shader.d.ts.map +1 -1
  45. package/build/physics/adapter.d.ts +3 -3
  46. package/build/physics/builtin/body.d.ts +23 -24
  47. package/build/physics/builtin/body.d.ts.map +1 -1
  48. package/build/physics/builtin/detector.d.ts +1 -1
  49. package/build/plugin/plugin.d.ts +9 -1
  50. package/build/plugin/plugin.d.ts.map +1 -1
  51. package/build/polyfill/index.d.ts +1 -4
  52. package/build/polyfill/index.d.ts.map +1 -1
  53. package/build/renderable/mesh.d.ts +16 -0
  54. package/build/renderable/mesh.d.ts.map +1 -1
  55. package/build/renderable/renderable.d.ts +16 -10
  56. package/build/renderable/renderable.d.ts.map +1 -1
  57. package/build/renderable/sprite.d.ts +7 -7
  58. package/build/renderable/sprite.d.ts.map +1 -1
  59. package/build/state/stage.d.ts +4 -2
  60. package/build/state/stage.d.ts.map +1 -1
  61. package/build/system/event.d.ts +1 -1
  62. package/build/system/platform.d.ts +21 -1
  63. package/build/system/platform.d.ts.map +1 -1
  64. package/build/utils/fetchdata.d.ts.map +1 -0
  65. package/build/video/blendmodes.d.ts +1 -1
  66. package/build/video/canvas/canvas_renderer.d.ts +5 -2
  67. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  68. package/build/video/effects/blur.d.ts +4 -4
  69. package/build/video/effects/blur.d.ts.map +1 -1
  70. package/build/video/effects/chromaticAberration.d.ts +5 -5
  71. package/build/video/effects/chromaticAberration.d.ts.map +1 -1
  72. package/build/video/effects/colorMatrix.d.ts +5 -5
  73. package/build/video/effects/colorMatrix.d.ts.map +1 -1
  74. package/build/video/effects/desaturate.d.ts +5 -5
  75. package/build/video/effects/desaturate.d.ts.map +1 -1
  76. package/build/video/effects/dissolve.d.ts +4 -4
  77. package/build/video/effects/dissolve.d.ts.map +1 -1
  78. package/build/video/effects/dropShadow.d.ts +5 -5
  79. package/build/video/effects/dropShadow.d.ts.map +1 -1
  80. package/build/video/effects/flash.d.ts +4 -4
  81. package/build/video/effects/flash.d.ts.map +1 -1
  82. package/build/video/effects/glow.d.ts +5 -5
  83. package/build/video/effects/glow.d.ts.map +1 -1
  84. package/build/video/effects/hologram.d.ts +4 -4
  85. package/build/video/effects/hologram.d.ts.map +1 -1
  86. package/build/video/effects/invert.d.ts +5 -5
  87. package/build/video/effects/invert.d.ts.map +1 -1
  88. package/build/video/effects/outline.d.ts +6 -6
  89. package/build/video/effects/outline.d.ts.map +1 -1
  90. package/build/video/effects/pixelate.d.ts +4 -4
  91. package/build/video/effects/pixelate.d.ts.map +1 -1
  92. package/build/video/effects/radialGradient.d.ts +2 -2
  93. package/build/video/effects/scanline.d.ts +6 -6
  94. package/build/video/effects/scanline.d.ts.map +1 -1
  95. package/build/video/effects/sepia.d.ts +5 -5
  96. package/build/video/effects/sepia.d.ts.map +1 -1
  97. package/build/video/effects/shadereffect.d.ts +12 -10
  98. package/build/video/effects/shadereffect.d.ts.map +1 -1
  99. package/build/video/effects/shine.d.ts +3 -3
  100. package/build/video/effects/shine.d.ts.map +1 -1
  101. package/build/video/effects/tintPulse.d.ts +4 -4
  102. package/build/video/effects/tintPulse.d.ts.map +1 -1
  103. package/build/video/effects/vignette.d.ts +6 -6
  104. package/build/video/effects/vignette.d.ts.map +1 -1
  105. package/build/video/effects/wave.d.ts +4 -4
  106. package/build/video/effects/wave.d.ts.map +1 -1
  107. package/build/video/texture/atlas.d.ts +1 -1
  108. package/build/video/webgl/glshader.d.ts +2 -2
  109. package/build/video/webgl/webgl_renderer.d.ts +2 -1
  110. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  111. package/build/video/webgpu/webgpu_renderer.d.ts +2 -1
  112. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  113. package/package.json +5 -9
  114. package/skills/AGENTS.md +67 -0
  115. package/skills/melonjs/SKILL.md +156 -0
  116. package/skills/melonjs-20-migration/SKILL.md +160 -0
  117. package/skills/melonjs-3d/SKILL.md +226 -0
  118. package/skills/melonjs-3d-assets/SKILL.md +239 -0
  119. package/skills/melonjs-audio/SKILL.md +215 -0
  120. package/skills/melonjs-camera-and-drawing/SKILL.md +171 -0
  121. package/skills/melonjs-deployment/SKILL.md +113 -0
  122. package/skills/melonjs-effects-and-shaders/SKILL.md +177 -0
  123. package/skills/melonjs-events/SKILL.md +159 -0
  124. package/skills/melonjs-getting-started/SKILL.md +183 -0
  125. package/skills/melonjs-input/SKILL.md +166 -0
  126. package/skills/melonjs-lighting/SKILL.md +139 -0
  127. package/skills/melonjs-loading-assets/SKILL.md +157 -0
  128. package/skills/melonjs-particles-and-trails/SKILL.md +136 -0
  129. package/skills/melonjs-performance/SKILL.md +178 -0
  130. package/skills/melonjs-physics/SKILL.md +321 -0
  131. package/skills/melonjs-plugins/SKILL.md +191 -0
  132. package/skills/melonjs-renderables/SKILL.md +214 -0
  133. package/skills/melonjs-renderer-backends/SKILL.md +173 -0
  134. package/skills/melonjs-scenes-and-state/SKILL.md +180 -0
  135. package/skills/melonjs-sprites-and-animation/SKILL.md +173 -0
  136. package/skills/melonjs-tilemaps/SKILL.md +193 -0
  137. package/skills/melonjs-ui-and-text/SKILL.md +175 -0
  138. package/build/audio/backend.d.ts.map +0 -1
  139. package/build/loader/parsers/fetchdata.d.ts.map +0 -1
  140. /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`