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,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-events
|
|
3
|
+
description: "Use this skill for the melonJS event bus and lifecycle hooks — subscribing to engine events, the per-frame and per-state event names, renderable lifecycle callbacks, and how to unsubscribe without leaking. Triggers on: event.on, event.off, event.once, event.emit, GAME_UPDATE, GAME_BEFORE_DRAW, LEVEL_LOADED, LOADER_COMPLETE, STATE_CHANGE, CANVAS_ONRESIZE, POINTERMOVE, WORLD_STEP, onActivateEvent, onDeactivateEvent, onDestroyEvent, lifecycle, subscribe, listener."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Events and lifecycle
|
|
8
|
+
|
|
9
|
+
## The bus
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { event } from "melonjs";
|
|
13
|
+
|
|
14
|
+
const onUpdate = () => { /* … */ };
|
|
15
|
+
const unsubscribe = event.on(event.GAME_UPDATE, onUpdate); // on() returns its own off()
|
|
16
|
+
event.once(event.LEVEL_LOADED, () => { /* … */ });
|
|
17
|
+
|
|
18
|
+
// two ways to undo it — the same reference, or the returned function
|
|
19
|
+
event.off(event.GAME_UPDATE, onUpdate);
|
|
20
|
+
unsubscribe();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`event.on`/`once`/`off`/`has` all take an optional third `context` argument,
|
|
24
|
+
invoked as the handler's `this`. **`off` matches on the pair — handler identity
|
|
25
|
+
AND context identity.** `on(EVT, fn, this)` followed by `off(EVT, fn)` removes
|
|
26
|
+
nothing, silently.
|
|
27
|
+
|
|
28
|
+
**Store the handler reference, or use the context argument.** An inline arrow or
|
|
29
|
+
a fresh `.bind(this)` in the `on` call can never be removed — that is the
|
|
30
|
+
standard way to leak listeners in melonJS.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// ✗ unremovable
|
|
34
|
+
event.on(event.GAME_UPDATE, this.tick.bind(this));
|
|
35
|
+
|
|
36
|
+
// ✓ keep the bound reference
|
|
37
|
+
this._tick = this.tick.bind(this);
|
|
38
|
+
event.on(event.GAME_UPDATE, this._tick);
|
|
39
|
+
// later: event.off(event.GAME_UPDATE, this._tick);
|
|
40
|
+
|
|
41
|
+
// ✓ or skip the bind entirely — the context is part of the identity
|
|
42
|
+
event.on(event.GAME_UPDATE, this.tick, this);
|
|
43
|
+
// later: event.off(event.GAME_UPDATE, this.tick, this);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Custom event names work at runtime (`event.emit("my.custom.event", payload)`),
|
|
47
|
+
but `on`/`emit` are typed against a closed map of the engine's own events, so in
|
|
48
|
+
TypeScript a name outside that map is a compile error. Use your own emitter for
|
|
49
|
+
game events in a `.ts` project.
|
|
50
|
+
|
|
51
|
+
## The events worth knowing
|
|
52
|
+
|
|
53
|
+
**Per frame**
|
|
54
|
+
|
|
55
|
+
| event | when |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `GAME_BEFORE_UPDATE` / `GAME_UPDATE` / `GAME_AFTER_UPDATE` | around the world update — `GAME_UPDATE` is **not** emitted while paused |
|
|
58
|
+
| `GAME_BEFORE_DRAW` / `GAME_AFTER_DRAW` | around the draw pass |
|
|
59
|
+
| `WORLD_STEP` | after the physics step |
|
|
60
|
+
| `TICK` | the raw engine tick |
|
|
61
|
+
|
|
62
|
+
**Lifecycle**
|
|
63
|
+
|
|
64
|
+
| event | when |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `BOOT`, `GAME_INIT`, `VIDEO_INIT` | engine start-up |
|
|
67
|
+
| `STATE_CHANGE`, `STATE_PAUSE`, `STATE_RESUME`, `STATE_STOP` | state machine |
|
|
68
|
+
| `STAGE_RESET` | a stage was reset |
|
|
69
|
+
| `LEVEL_LOADED` | a Tiled level finished loading |
|
|
70
|
+
| `LOADER_PROGRESS`, `LOADER_COMPLETE`, `LOADER_ERROR` | asset loading |
|
|
71
|
+
| `BLUR` / `FOCUS` | the page lost or regained focus |
|
|
72
|
+
| `CANVAS_ONRESIZE`, `VIEWPORT_ONRESIZE` | relayout hooks |
|
|
73
|
+
| `POINTERMOVE` | global pointer movement |
|
|
74
|
+
|
|
75
|
+
Use `LOADER_PROGRESS` / `LOADER_COMPLETE`. The `loader.onProgress` /
|
|
76
|
+
`loader.onload` / `loader.onError` properties were deprecated in 18.2.0 and
|
|
77
|
+
removed in 20.3: they were module-level `let` bindings on an ES module
|
|
78
|
+
namespace, so `loader.onProgress = fn` threw and the documented migration path
|
|
79
|
+
never worked. For completion, `loader.preload(assets, onloadcb)` also takes the
|
|
80
|
+
callback directly.
|
|
81
|
+
|
|
82
|
+
## Renderable lifecycle callbacks
|
|
83
|
+
|
|
84
|
+
Prefer these over bus subscriptions where they fit — the engine calls them at the
|
|
85
|
+
right times and there is nothing to unsubscribe:
|
|
86
|
+
|
|
87
|
+
| callback | when |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `onActivateEvent()` | `addChild`-ed into a container that is already attached to the world root (a `Container` forwards it to its children) |
|
|
90
|
+
| `onDeactivateEvent()` | removed from its container — including the automatic world clear on a state change |
|
|
91
|
+
| `onDestroyEvent()` | `destroy()` ran; called with no arguments from the container-removal path |
|
|
92
|
+
| `onCollisionStart` / `onCollisionActive` / `onCollisionEnd` | contacts |
|
|
93
|
+
|
|
94
|
+
Note the activate gate: adding a child to a *detached* container fires nothing.
|
|
95
|
+
The callback arrives later, when that container itself is added to the world.
|
|
96
|
+
And an `isPersistent` renderable survives the world clear between stages, so it
|
|
97
|
+
gets no fresh `onActivateEvent` on the next state.
|
|
98
|
+
|
|
99
|
+
**Pair subscriptions with activate/deactivate, not the constructor.** This is
|
|
100
|
+
the rule that keeps pooled objects working:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
onActivateEvent() {
|
|
104
|
+
this._tick = this.update.bind(this);
|
|
105
|
+
event.on(event.GAME_UPDATE, this._tick);
|
|
106
|
+
input.registerPointerEvent("pointerdown", this, this._onDown);
|
|
107
|
+
}
|
|
108
|
+
onDeactivateEvent() {
|
|
109
|
+
event.off(event.GAME_UPDATE, this._tick);
|
|
110
|
+
input.releasePointerEvent("pointerdown", this);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Pool-recycled objects never fire `onDestroyEvent` on removal** — `removeChildNow`
|
|
115
|
+
tries `pool.push(child)` first and only calls `destroy()` if that fails, so a
|
|
116
|
+
class registered with `pool.register(name, Class, true)` goes back to the pool
|
|
117
|
+
instead. Anything you clean up there will not run. `destroy()` is also what calls
|
|
118
|
+
`releaseAllPointerEvents(this)`, which is why a pooled object must release its
|
|
119
|
+
own pointer registrations in `onDeactivateEvent`.
|
|
120
|
+
|
|
121
|
+
## Guard callbacks that can fire during teardown
|
|
122
|
+
|
|
123
|
+
Events can arrive while a stage is being dismantled, when the current state is
|
|
124
|
+
already something else:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
if (!state.isCurrent(state.PLAY)) return;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Without this, a "children emptied" handler can reset the wrong stage.
|
|
131
|
+
|
|
132
|
+
## Removing objects from a collision callback
|
|
133
|
+
|
|
134
|
+
Contact callbacks fire inline inside the physics step, mid-iteration over the
|
|
135
|
+
detector's pair list. `container.removeChild()` is safe there — it defers the
|
|
136
|
+
actual removal to the next tick, which is exactly why it defers. The immediate
|
|
137
|
+
forms are the dangerous ones: `removeChildNow()` and `destroy()` tear the object
|
|
138
|
+
down under the iteration (`destroy()` nulls `pos` and `body`). The builtin
|
|
139
|
+
detector guards the pair it is currently resolving, but nothing guarantees that
|
|
140
|
+
for the rest of the step or for another adapter.
|
|
141
|
+
|
|
142
|
+
Use `removeChild()`, or flag the work and drain it from `Stage.update`.
|
|
143
|
+
|
|
144
|
+
## Symptom → cause
|
|
145
|
+
|
|
146
|
+
| symptom | cause |
|
|
147
|
+
|---|---|
|
|
148
|
+
| handler still fires after removing the object | `event.off` given a different function reference, or a different `context` than `on` got |
|
|
149
|
+
| handlers accumulate on pooled objects | subscribed in the constructor rather than `onActivateEvent` |
|
|
150
|
+
| cleanup never runs on a pooled object | pooled removal does not fire `onDestroyEvent` |
|
|
151
|
+
| crash during a stage switch | callback ran mid-teardown — guard with `state.isCurrent` |
|
|
152
|
+
| crash after a collision | `removeChildNow()` / `destroy()` inside a contact callback — use `removeChild()` |
|
|
153
|
+
| progress callbacks never fire | assigned `loader.onProgress` (removed in 20.3) instead of subscribing to `LOADER_PROGRESS` |
|
|
154
|
+
| `onActivateEvent` never runs | the child was added to a container not yet attached to the world |
|
|
155
|
+
|
|
156
|
+
## Related skills
|
|
157
|
+
|
|
158
|
+
- `melonjs-scenes-and-state` — the state machine and stage lifecycle
|
|
159
|
+
- `melonjs-performance` — pooling, and why activate/deactivate pairing matters
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-getting-started
|
|
3
|
+
description: "Use this skill when starting or bootstrapping a melonJS game, creating the Application, choosing a renderer, or setting up the first scene. Covers the mandatory async init, the world container, adding renderables, and the WebGPU/WebGL 2/Canvas fallback. Triggers on: melonjs, new Application, app.init, app.world, video.AUTO, video.WEBGL, video.CANVAS, Stage, state.change, getting started, bootstrap."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Getting started with melonJS
|
|
8
|
+
|
|
9
|
+
> Bootstrapping a melonJS 20.x game: the `Application` lifecycle, the mandatory
|
|
10
|
+
> `await app.init()`, the world container, and what the renderer fallback means
|
|
11
|
+
> for feature availability.
|
|
12
|
+
|
|
13
|
+
**Applies to:** melonJS 20.x. Older major versions differ — see the
|
|
14
|
+
`melonjs-20-migration` skill.
|
|
15
|
+
|
|
16
|
+
## The bootstrap
|
|
17
|
+
|
|
18
|
+
`Application` construction is synchronous but **initialisation is not**. `init()`
|
|
19
|
+
returns a promise and must be awaited before building any renderable, loading
|
|
20
|
+
assets, or entering a scene.
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { Application, Text } from "melonjs";
|
|
24
|
+
|
|
25
|
+
const app = new Application(1218, 562, {
|
|
26
|
+
parent: "screen", // id of the DOM element to mount into
|
|
27
|
+
scale: "auto",
|
|
28
|
+
backgroundColor: "#202020",
|
|
29
|
+
});
|
|
30
|
+
await app.init(); // ← REQUIRED. Nothing below works without it.
|
|
31
|
+
|
|
32
|
+
app.world.addChild(
|
|
33
|
+
new Text(609, 281, {
|
|
34
|
+
font: "Arial",
|
|
35
|
+
size: 160,
|
|
36
|
+
fillStyle: "#FFFFFF",
|
|
37
|
+
textAlign: "center",
|
|
38
|
+
textBaseline: "middle",
|
|
39
|
+
text: "Hello World !",
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**This is the single most common thing to get wrong.** On 19.x the constructor
|
|
45
|
+
called `init(width, height, options)` for you; on 20.x `init()` takes no
|
|
46
|
+
arguments and calling — and awaiting — it is your job. Nothing that follows
|
|
47
|
+
names the missing call. The constructor does build `app.world`, so adding a
|
|
48
|
+
plain renderable succeeds and simply never draws — no canvas is appended and the
|
|
49
|
+
app never subscribes to the frame tick — while constructing a `Sprite` or a
|
|
50
|
+
`Text` throws `Cannot read properties of undefined (reading 'renderer')`,
|
|
51
|
+
because the global `game` those two fall back on is only set once the first
|
|
52
|
+
`init()` resolves. Either way `app.renderer` and `app.viewport` stay `undefined`
|
|
53
|
+
and the page stays blank.
|
|
54
|
+
|
|
55
|
+
`init()` is asynchronous because acquiring a WebGPU device is asynchronous. There
|
|
56
|
+
is no synchronous alternative.
|
|
57
|
+
|
|
58
|
+
## Top-level await
|
|
59
|
+
|
|
60
|
+
The bootstrap needs `await` at module scope. In an ES module that is fine. If the
|
|
61
|
+
surrounding tooling does not allow it, wrap it:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
async function main() {
|
|
65
|
+
const app = new Application(800, 600, { parent: "screen" });
|
|
66
|
+
await app.init();
|
|
67
|
+
// …
|
|
68
|
+
}
|
|
69
|
+
void main();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Choosing a renderer
|
|
73
|
+
|
|
74
|
+
`video.AUTO` (the default) tries **WebGPU → WebGL 2 → Canvas** and uses the first
|
|
75
|
+
that works.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import { video } from "melonjs";
|
|
79
|
+
|
|
80
|
+
const app = new Application(800, 600, {
|
|
81
|
+
parent: "screen",
|
|
82
|
+
renderer: video.AUTO, // default; or video.WEBGPU / video.WEBGL / video.CANVAS
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Two consequences worth knowing before writing rendering code:
|
|
87
|
+
|
|
88
|
+
- **Which backend you get is a runtime fact, not a build-time one.** The same
|
|
89
|
+
code runs on a different renderer depending on the machine. Do not assume
|
|
90
|
+
WebGL.
|
|
91
|
+
- **Not every feature exists on every backend.** Shader effects and post effects
|
|
92
|
+
need a programmable pipeline and are inert on Canvas. Where a feature degrades,
|
|
93
|
+
the engine warns once rather than throwing — so a missing effect is a console
|
|
94
|
+
warning, not a crash.
|
|
95
|
+
|
|
96
|
+
Pin the renderer explicitly (`video.WEBGL`) only when you have a reason to. It
|
|
97
|
+
costs you the WebGPU path and the Canvas fallback.
|
|
98
|
+
|
|
99
|
+
## The scene graph
|
|
100
|
+
|
|
101
|
+
`app.world` is the root `Container`. Everything drawn is a descendant of it.
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
app.world.addChild(sprite, 10); // 10 is the z / draw order
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Pass the z-order as the second argument to `addChild`** — it is the only form
|
|
108
|
+
that works. `renderable.z` does not exist as a property, setting `depth` before
|
|
109
|
+
`addChild` is overwritten by `autoDepth`, and setting it afterwards does not
|
|
110
|
+
resort. See the `melonjs-renderables` skill for the full explanation.
|
|
111
|
+
|
|
112
|
+
## Scenes
|
|
113
|
+
|
|
114
|
+
For anything beyond a single screen, use `Stage` subclasses and the `state`
|
|
115
|
+
manager rather than mutating `app.world` directly:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import { Stage, state } from "melonjs";
|
|
119
|
+
|
|
120
|
+
class PlayScreen extends Stage {
|
|
121
|
+
onResetEvent() { /* build the scene */ }
|
|
122
|
+
onDestroyEvent() { /* tear it down */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
state.set(state.PLAY, new PlayScreen());
|
|
126
|
+
state.change(state.PLAY);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`onResetEvent` runs every time the state is entered, not once — build scene
|
|
130
|
+
content there, not in the constructor.
|
|
131
|
+
|
|
132
|
+
## Loading assets
|
|
133
|
+
|
|
134
|
+
Assets are declared and preloaded through `loader`; audio needs `audio.init()`
|
|
135
|
+
with the formats you ship before any audio asset is preloaded.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import { audio, loader } from "melonjs";
|
|
139
|
+
|
|
140
|
+
loader.setOptions({ crossOrigin: "anonymous" }); // needed for remote assets
|
|
141
|
+
audio.init("mp3,ogg"); // BEFORE preloading audio
|
|
142
|
+
await loader.preload([
|
|
143
|
+
{ name: "tileset", type: "image", src: "data/img/tileset.png" },
|
|
144
|
+
{ name: "jump", type: "audio", src: "data/sfx/" }, // directory, not file
|
|
145
|
+
]);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Two ordering rules that produce confusing failures when missed:
|
|
149
|
+
|
|
150
|
+
- **`audio.init()` must run before any audio asset is preloaded**, and names the
|
|
151
|
+
formats you actually ship. Preloading first throws *"target audio extension(s)
|
|
152
|
+
should be set through me.audio.init() before calling the preloader"*.
|
|
153
|
+
- The audio `src` is a **directory**, not a file. The loader appends the asset
|
|
154
|
+
`name` plus each configured extension, so a full filename gives you a 404.
|
|
155
|
+
|
|
156
|
+
## `anchorPoint` defaults to the centre
|
|
157
|
+
|
|
158
|
+
`Renderable.anchorPoint` is `(0.5, 0.5)`, so `pos` is an object's **centre**, not
|
|
159
|
+
its top-left. `Container` forces `(0, 0)` instead.
|
|
160
|
+
|
|
161
|
+
Writing top-left math against a centred anchor puts everything half a sprite off
|
|
162
|
+
with no error — it is the most common source of "close but subtly wrong"
|
|
163
|
+
placement.
|
|
164
|
+
|
|
165
|
+
## Common mistakes
|
|
166
|
+
|
|
167
|
+
| symptom | cause |
|
|
168
|
+
|---|---|
|
|
169
|
+
| blank page, nothing drawn, or a `TypeError` reading `renderer` from `new Sprite` / `new Text` | missing `await app.init()` |
|
|
170
|
+
| objects draw in the wrong order | `.z`/`depth` set instead of `addChild(child, z)` — `.z` is not even a real property |
|
|
171
|
+
| shader/post effect does nothing, warning in console | running on the Canvas renderer |
|
|
172
|
+
| audio 404s | audio asset `src` given as a file rather than a directory |
|
|
173
|
+
| scene content appears once and never resets | scene built in the constructor instead of `onResetEvent` |
|
|
174
|
+
| everything offset by half a sprite | top-left math against the centred `anchorPoint` default |
|
|
175
|
+
| a `Tween` or `ParticleEmitter` does nothing | `.start()` / `streamParticles()` never called |
|
|
176
|
+
| cross-origin textures fail to load | missing `loader.setOptions({ crossOrigin: "anonymous" })` |
|
|
177
|
+
|
|
178
|
+
## Related skills
|
|
179
|
+
|
|
180
|
+
- `melonjs-20-migration` — what changed in 20.x that older code and older
|
|
181
|
+
training data get wrong
|
|
182
|
+
- `melonjs-renderables` — custom `Renderable` subclasses, pointer events, post
|
|
183
|
+
effects and the traps in each
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-input
|
|
3
|
+
description: "Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, click, touch, drag."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Input: keyboard, pointer and gamepad
|
|
8
|
+
|
|
9
|
+
## Keyboard goes through named actions
|
|
10
|
+
|
|
11
|
+
Do not poll key codes. Bind a key to an **action string**, then poll the action —
|
|
12
|
+
this is what lets gamepad and virtual controls map onto the same handling.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { input } from "melonjs";
|
|
16
|
+
|
|
17
|
+
input.bindKey(input.KEY.LEFT, "left");
|
|
18
|
+
input.bindKey(input.KEY.RIGHT, "right");
|
|
19
|
+
input.bindKey(input.KEY.X, "jump", true); // true = locked: one press per keydown
|
|
20
|
+
|
|
21
|
+
// in update()
|
|
22
|
+
if (input.isKeyPressed("left")) { /* … */ }
|
|
23
|
+
if (input.isKeyPressed("jump")) { /* fires once per press, not per frame */ }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The third argument to `bindKey` is `lock` (default `false`). Without it,
|
|
27
|
+
`isKeyPressed` stays true for as long as the key is held — correct for movement,
|
|
28
|
+
wrong for jump or fire. A locked action stays consumed until the key is released
|
|
29
|
+
or you call `input.unlockKey("jump")`. Use `input.keyStatus(action)` when you
|
|
30
|
+
want the raw held/not-held state regardless of locking.
|
|
31
|
+
|
|
32
|
+
The full signature is `bindKey(keyCode, action, lock?, preventDefault?)`;
|
|
33
|
+
`preventDefault` defaults to `input.preventDefault` (itself `true`). Pass the
|
|
34
|
+
fourth argument per binding, or change the global default with
|
|
35
|
+
`input.setPreventDefault(false)` — **assigning `input.preventDefault = false`
|
|
36
|
+
does not work**, it is a read-only module binding.
|
|
37
|
+
|
|
38
|
+
## Pointer events need `isKinematic = false`
|
|
39
|
+
|
|
40
|
+
**This is the trap.** `Renderable.isKinematic` defaults to `true`, and both the
|
|
41
|
+
broadphase and the pointer dispatcher skip any candidate whose `isKinematic` is
|
|
42
|
+
still `true`. The handler registers successfully and simply never fires.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
class Button extends Renderable {
|
|
46
|
+
constructor(x, y, w, h) {
|
|
47
|
+
super(x, y, w, h);
|
|
48
|
+
this.isKinematic = false; // ← required
|
|
49
|
+
}
|
|
50
|
+
onActivateEvent() {
|
|
51
|
+
input.registerPointerEvent("pointerdown", this, this.onDown.bind(this));
|
|
52
|
+
}
|
|
53
|
+
onDeactivateEvent() {
|
|
54
|
+
input.releasePointerEvent("pointerdown", this);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*` elements set it
|
|
60
|
+
to `false` for you, and so does attaching a physics body (`Body`'s constructor
|
|
61
|
+
sets `ancestor.isKinematic = false`) — so plain `Renderable` and `Sprite`
|
|
62
|
+
subclasses with no body are the ones that catch people.
|
|
63
|
+
|
|
64
|
+
Register in `onActivateEvent` and release in `onDeactivateEvent`, not in the
|
|
65
|
+
constructor — pooled objects are reused and would otherwise accumulate handlers.
|
|
66
|
+
|
|
67
|
+
`app.viewport` works as a whole-screen region when you want global clicks: the
|
|
68
|
+
dispatcher appends it to the candidate list on every event, so it is reachable
|
|
69
|
+
even when nothing else is under the pointer.
|
|
70
|
+
|
|
71
|
+
The accepted event names are exactly `"pointerdown"`, `"pointerup"`,
|
|
72
|
+
`"pointermove"`, `"pointercancel"`, `"pointerenter"`, `"pointerover"`,
|
|
73
|
+
`"pointerleave"` and `"wheel"`. Anything else — `"click"`, `"mousedown"`,
|
|
74
|
+
`"touchstart"` — makes both `registerPointerEvent` and `releasePointerEvent`
|
|
75
|
+
throw `invalid event type`. melonJS maps the canonical name onto whichever
|
|
76
|
+
mouse/touch events the device actually supports, so you always register the
|
|
77
|
+
pointer name.
|
|
78
|
+
|
|
79
|
+
## World coordinates versus screen coordinates
|
|
80
|
+
|
|
81
|
+
The pointer carries several, and picking the wrong one produces a feedback loop:
|
|
82
|
+
|
|
83
|
+
| property | meaning | use for |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `pointer.gameScreenX` / `gameScreenY` | canvas-relative position | anything that **moves the camera** |
|
|
86
|
+
| `pointer.gameWorldX` / `gameWorldY` | projected into world space | picking and spawning objects |
|
|
87
|
+
| `pointer.gameLocalX` / `gameLocalY` | relative to the region's ancestor container | hit-testing inside a container |
|
|
88
|
+
| `pointer.gameX` / `gameY` | **context-dependent** — see below | region handlers on non-floating regions |
|
|
89
|
+
|
|
90
|
+
`gameX`/`gameY` is not a fixed coordinate space. The dispatcher rewrites it per
|
|
91
|
+
candidate just before calling your handler: world coordinates for a normal
|
|
92
|
+
region, screen coordinates for a `isFloating === true` region. On the global
|
|
93
|
+
`event.POINTERMOVE` broadcast it is set to the **screen** coordinates. When you
|
|
94
|
+
need one specific space, read `gameWorldX`/`gameWorldY` or
|
|
95
|
+
`gameScreenX`/`gameScreenY` by name rather than relying on `gameX`.
|
|
96
|
+
|
|
97
|
+
Using world coordinates for a camera-orbit drag means the same pixel maps to a
|
|
98
|
+
different world point every frame as the camera moves — the drag jumps wildly.
|
|
99
|
+
|
|
100
|
+
## Pointer move and release outside a region
|
|
101
|
+
|
|
102
|
+
Region handlers only fire while the pointer is over the region. For drag
|
|
103
|
+
tracking, listen globally:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import { event } from "melonjs";
|
|
107
|
+
event.on(event.POINTERMOVE, (pointer) => { /* … */ });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
There is **no global pointer-up system event** — `event.ts` publishes only
|
|
111
|
+
`POINTERMOVE`, `POINTERLOCKCHANGE`, `DRAGSTART` and `DRAGEND`. For "released
|
|
112
|
+
anywhere" you need a raw listener:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
globalThis.addEventListener("pointerup", onRelease);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
For plain drag-and-drop, `Draggable` / `DropTarget` already wire all of this up
|
|
119
|
+
(`pointerdown` / `pointerup` / `pointercancel` regions plus a global
|
|
120
|
+
`POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`. `DRAGEND`
|
|
121
|
+
still originates from a region handler, so it only fires when the release lands
|
|
122
|
+
on the draggable itself.
|
|
123
|
+
|
|
124
|
+
## Gamepad
|
|
125
|
+
|
|
126
|
+
Gamepad input maps onto the same action strings, so game code does not branch:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
input.bindGamepad(0,
|
|
130
|
+
{ type: "buttons", code: input.GAMEPAD.BUTTONS.FACE_1 },
|
|
131
|
+
input.KEY.X); // → the "jump" action bound above
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Order matters:** `bindGamepad` throws `no action defined for keycode N` if the
|
|
135
|
+
keycode has not already been passed to `bindKey`. Axes use the same call with a
|
|
136
|
+
threshold: `{ type: "axes", code: input.GAMEPAD.AXES.LX, threshold: -0.5 }`.
|
|
137
|
+
`unbindGamepad(index, button)`, `setGamepadDeadzone(value)` and
|
|
138
|
+
`setGamepadMapping(id, mapping)` complete the surface.
|
|
139
|
+
|
|
140
|
+
## Virtual controls
|
|
141
|
+
|
|
142
|
+
On-screen buttons synthesise key events, again reaching the same actions:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
input.triggerKeyEvent(input.KEY.LEFT, true); // press
|
|
146
|
+
input.triggerKeyEvent(input.KEY.LEFT, false); // release
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Symptom → cause
|
|
150
|
+
|
|
151
|
+
| symptom | cause |
|
|
152
|
+
|---|---|
|
|
153
|
+
| pointer handler never fires | `isKinematic` left at its `true` default |
|
|
154
|
+
| handlers fire twice, or on dead objects | registered in the constructor instead of `onActivateEvent` |
|
|
155
|
+
| camera drag jumps around | using world coordinates instead of `gameScreenX`/`gameScreenY` |
|
|
156
|
+
| `gameX` holds screen coords where world coords were expected | read on `event.POINTERMOVE`, or on a floating region — use `gameWorldX`/`gameWorldY` |
|
|
157
|
+
| drag stops when leaving the object | region events end at the region — use `event.POINTERMOVE` |
|
|
158
|
+
| release never detected outside the region | no global pointer-up event — add a raw listener |
|
|
159
|
+
| jump fires every frame while held | `bindKey` without its third `lock` argument |
|
|
160
|
+
| `invalid event type` thrown at registration | non-pointer name (`"click"`, `"mousedown"`, …) passed to `registerPointerEvent` |
|
|
161
|
+
| `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |
|
|
162
|
+
|
|
163
|
+
## Related skills
|
|
164
|
+
|
|
165
|
+
- `melonjs-renderables` — `isKinematic` also gates the physics broadphase
|
|
166
|
+
- `melonjs-ui-and-text` — `UISpriteElement` and `UITextButton` handle this for you
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-lighting
|
|
3
|
+
description: "Use this skill for 2D dynamic lighting in melonJS — Light2d, ambient light on a Stage, multiple lights, and per-pixel normal-mapped sprites authored in SpriteIlluminator. Covers the ambient-cutout model, which parts need a GPU backend, and why Light2d does not survive a Camera3d. Triggers on: Light2d, lighting, ambientLight, ambientLightingColor, normalMap, SpriteIlluminator, per-pixel lighting, drawLight, torch, glow, darkness."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 2D dynamic lighting
|
|
8
|
+
|
|
9
|
+
> `Light2d` is a world renderable that cuts through a stage's ambient darkness.
|
|
10
|
+
> The glow and the cutout work on every backend; only per-pixel normal-map
|
|
11
|
+
> shading needs a GPU one. It is 2D-only.
|
|
12
|
+
|
|
13
|
+
## The model: ambient darkness plus cutouts
|
|
14
|
+
|
|
15
|
+
The `Stage` holds an ambient colour — a dark sheet painted over the scene after
|
|
16
|
+
the world is drawn — and each active `Light2d` punches a hole in it at its
|
|
17
|
+
visible area. The lights themselves are drawn additively on top.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
class PlayScreen extends Stage {
|
|
21
|
+
onResetEvent(app) {
|
|
22
|
+
this.ambientLight.parseCSS("#1a1a2e"); // the darkness
|
|
23
|
+
|
|
24
|
+
app.world.addChild(new Light2d(x, y, radiusX, radiusY, "#ffddaa", 0.9));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The constructor is `Light2d(x, y, radiusX, radiusY = radiusX, color = "#FFF",
|
|
30
|
+
intensity = 0.7)`, and `pos` is the light's **centre** — its `anchorPoint` is
|
|
31
|
+
`(0.5, 0.5)`, and `centerX` / `centerY` are overridden to return `pos` directly.
|
|
32
|
+
Give `radiusY` a different value for a stretched, elliptical light.
|
|
33
|
+
|
|
34
|
+
A `Light2d` added to the world **registers itself with the stage's ambient
|
|
35
|
+
overlay on activate**, so you add it like any other renderable and it takes part
|
|
36
|
+
in the lighting pass. It draws additively (`blendMode = "lighter"`); set
|
|
37
|
+
`light.illuminationOnly = true` to suppress the visible glow while still lighting
|
|
38
|
+
normal-mapped sprites.
|
|
39
|
+
|
|
40
|
+
`Stage.ambientLight` defaults to fully transparent black, and `drawLighting()`
|
|
41
|
+
returns immediately while its alpha is 0 — so with no ambient colour set there
|
|
42
|
+
is nothing to cut through and the lights just look like soft additive blobs.
|
|
43
|
+
|
|
44
|
+
## Multiple lights
|
|
45
|
+
|
|
46
|
+
Add as many as you need — the glow quads and the ambient cutouts accumulate with
|
|
47
|
+
no fixed cap.
|
|
48
|
+
|
|
49
|
+
The **normal-map** pipeline is capped: lights are packed into a std140 uniform
|
|
50
|
+
block sized for `MAX_LIGHTS = 32`, and anything past the 32nd is silently dropped
|
|
51
|
+
from the per-pixel shading (it still draws its glow and punches its cutout). The
|
|
52
|
+
cap is an allocation, not a budget — the fragment loop still runs once per pixel
|
|
53
|
+
per live light.
|
|
54
|
+
|
|
55
|
+
Lights are positioned like any renderable, so they can follow an entity by
|
|
56
|
+
updating `pos`, or be added as children of one.
|
|
57
|
+
|
|
58
|
+
## Per-pixel lighting with normal maps
|
|
59
|
+
|
|
60
|
+
For sprites that respond to light direction rather than just brightness, supply a
|
|
61
|
+
normal map alongside the diffuse texture:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
await loader.preload([
|
|
65
|
+
{ name: "hero", type: "image", src: "data/img/hero.png" },
|
|
66
|
+
{ name: "hero_n", type: "image", src: "data/img/hero_n.png" },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const hero = new Sprite(x, y, {
|
|
70
|
+
image: "hero",
|
|
71
|
+
normalMap: "hero_n", // ← enables per-pixel lighting on this sprite
|
|
72
|
+
framewidth: 32, frameheight: 32,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`normalMap` takes a loader key, or any image-like source (`HTMLImageElement`,
|
|
77
|
+
canvas, `ImageBitmap`, a `Texture2d`). Assigning anything else — including an
|
|
78
|
+
`HTMLVideoElement`, which would freeze on frame 0 — throws a `TypeError`.
|
|
79
|
+
|
|
80
|
+
Normal maps are typically authored in **SpriteIlluminator**. The sidecar pattern
|
|
81
|
+
above — one `_n` image beside each diffuse image — is the simplest setup, but an
|
|
82
|
+
atlas can carry one too: `new TextureAtlas(json, image, { normalMap })` pairs a
|
|
83
|
+
normal texture sharing the colour texture's UVs, and a `Sprite` built from that
|
|
84
|
+
atlas picks it up in preference to its own `settings.normalMap`.
|
|
85
|
+
|
|
86
|
+
Unlit areas of a normal-mapped sprite render pure black unless you raise
|
|
87
|
+
`Stage.ambientLightingColor` (default black) — that is the base level added to
|
|
88
|
+
every lit pixel, and it is a different knob from `Stage.ambientLight`.
|
|
89
|
+
|
|
90
|
+
Without a normal map a sprite is still lit, but flatly: brightness varies with
|
|
91
|
+
distance from the light, not with surface direction.
|
|
92
|
+
|
|
93
|
+
## What needs a GPU backend, and what does not
|
|
94
|
+
|
|
95
|
+
Less than you would expect. `Light2d` is renderer-agnostic: it just calls
|
|
96
|
+
`renderer.drawLight(this)`, and every backend implements it — the GPU backends
|
|
97
|
+
through a shared procedural radial-falloff shader, the Canvas renderer by
|
|
98
|
+
rasterising a cached `Gradient` into one shared render target and compositing it
|
|
99
|
+
with `drawImage`. The ambient cutout pass is plain `setMask` + `fillRect`. So the
|
|
100
|
+
lit-spot look works everywhere, Canvas included.
|
|
101
|
+
|
|
102
|
+
What Canvas cannot do is **per-pixel normal-map lighting**:
|
|
103
|
+
`renderer.setLightUniforms()` is a no-op there, and the first frame that passes a
|
|
104
|
+
non-empty light list logs a one-shot warning. Normal-mapped sprites simply render
|
|
105
|
+
unlit — no error.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
if (app.renderer.shaderLanguage === null) {
|
|
109
|
+
// Canvas fallback: lights and ambient still work, but `normalMap` is ignored
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Do not combine with `Camera3d`
|
|
114
|
+
|
|
115
|
+
`Camera3d` extends `Camera2d` and therefore still runs the 2D lighting pass, but
|
|
116
|
+
nothing in that pass is projection-aware: the ambient fill is a screen-space
|
|
117
|
+
`fillRect`, the cutout is a 2D `Ellipse` mask, and light positions are packed as
|
|
118
|
+
2D world coordinates minus the camera translate. Under a perspective projection
|
|
119
|
+
none of that lines up with what the camera actually draws.
|
|
120
|
+
|
|
121
|
+
For 3D scenes use `Light3d` instead — it is the counterpart for the mesh
|
|
122
|
+
lighting path, with `"directional"`, `"ambient"`, `"point"` and `"spot"` types.
|
|
123
|
+
See the `melonjs-3d` skill.
|
|
124
|
+
|
|
125
|
+
## Symptom → cause
|
|
126
|
+
|
|
127
|
+
| symptom | cause |
|
|
128
|
+
|---|---|
|
|
129
|
+
| lights added but the scene never darkens | no ambient colour set — `ambientLight` alpha is 0, so the pass returns early |
|
|
130
|
+
| sprite lit flatly, ignoring light direction | no `normalMap` on the sprite, or the Canvas backend is active |
|
|
131
|
+
| normal-mapped sprite is pure black where unlit | `Stage.ambientLightingColor` left at black |
|
|
132
|
+
| the 33rd light shades nothing | `MAX_LIGHTS = 32` for the normal-map pipeline |
|
|
133
|
+
| `TypeError` assigning `normalMap` | not an image-like value (a video element is rejected outright) |
|
|
134
|
+
| lighting misaligned under a 3D camera | the 2D light pass is not projection-aware — use `Light3d` |
|
|
135
|
+
|
|
136
|
+
## Related skills
|
|
137
|
+
|
|
138
|
+
- `melonjs-3d` — `Light3d` for perspective scenes
|
|
139
|
+
- `melonjs-sprites-and-animation` — sprite settings and atlases
|