From 10f05f5720f226fe3ebd9f204c4fd5b3e29d3b71 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 15 Sep 2026 07:33:16 +0800 Subject: [PATCH 1/2] skills: align physics, UI and pooling with the source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked the platformer example against the skills, taking the engine source as the authority. Three gaps, and one thing I had previously got wrong. - physics: `setMaxVelocity` and `setFriction` appeared in no skill, though `maxVel` is what turns `body.force` into a speed and the movement section already showed `force.x = maxVel.x`. Both are current, and both take an optional third argument for the depth axis - ui: the skill's description promises `UISpriteElement` but its body never named the handlers. Documents `onClick` / `onRelease` / `onOver` / `onOut` / `onHold`, that a `false` return stops propagation, and that the pointer only reaches an element whose `isKinematic` is false — which `UIBaseElement` and `UISpriteElement` clear themselves (uibaseelement.ts:86, uispriteelement.ts:95) - performance: CORRECTION. A previous commit claimed `pool.pull("me.Tween")` named something never registered, and rewrote the line to say registered names carry no `me.` prefix. Both claims are false: `pool.register` aliases every name under an `me.` prefix pointing at the same entry, in the pool AND the Tiled object factory (legacy_pool.js:87-97). That is why a map authored against 1.x still resolves its classes. Both spellings work; the unprefixed one is canonical Deliberately NOT propagated from the example: its platform and slope handling uses `response.overlapV` / `response.overlap`, which the physics skill correctly marks deprecated in favour of `depth` / `normal`. The skill documents the current adapter API; the example is legacy there. Likewise its `game.world` usage and `this.z = Number.POSITIVE_INFINITY`, both of which the skills already call out as wrong. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t --- .../skills/melonjs-performance/SKILL.md | 10 ++++-- .../melonjs/skills/melonjs-physics/SKILL.md | 13 +++++++ .../skills/melonjs-ui-and-text/SKILL.md | 34 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/melonjs/skills/melonjs-performance/SKILL.md b/packages/melonjs/skills/melonjs-performance/SKILL.md index 6cf5bbe0b..9198360af 100644 --- a/packages/melonjs/skills/melonjs-performance/SKILL.md +++ b/packages/melonjs/skills/melonjs-performance/SKILL.md @@ -81,11 +81,17 @@ Two rules that cause subtle bugs when missed: successfully recycled object never sees it. Pair event subscriptions with `onActivateEvent` / `onDeactivateEvent` instead, or you leak handlers. -Engine classes are poolable too: `pool.pull("Tween", target)`. The registered -names carry no `me.` prefix — `Entity`, `Collectable`, `Trigger`, `Light2d`, +Engine classes are poolable too: `pool.pull("Tween", target)`. The canonical +names are unprefixed — `Entity`, `Collectable`, `Trigger`, `Light2d`, `Particle`, `Sprite`, `NineSliceSprite`, `Renderable`, `Text`, `BitmapText`, `ImageLayer`, `Tween`, `ColorLayer`. +`pool.register` additionally aliases every name under an `me.` prefix, pointing +at the same entry, so `pool.pull("me.Tween")` resolves identically — and the +same alias is registered with the Tiled object factory, which is why a map +authored against melonJS 1.x still finds its classes. Prefer the unprefixed +name in new code; do not "correct" an `me.`-prefixed one, it is not broken. + ## `scale()` is multiplicative A pooled sprite arrives carrying its previous life's transform, and calling diff --git a/packages/melonjs/skills/melonjs-physics/SKILL.md b/packages/melonjs/skills/melonjs-physics/SKILL.md index 3eb53436c..1c1736ca3 100644 --- a/packages/melonjs/skills/melonjs-physics/SKILL.md +++ b/packages/melonjs/skills/melonjs-physics/SKILL.md @@ -120,6 +120,19 @@ this.body.setVelocity(vx, vy); this.body.force.x = this.body.maxVel.x; ``` +`maxVel` is what turns a force into a speed, so the two are set together. Use +the setters rather than writing the vectors — both take an optional THIRD +argument for the depth axis, left unchanged when omitted, so a 2D call keeps +its behaviour: + +```js +this.body.setMaxVelocity(3, 15); // walk speed, jump speed +this.body.setFriction(0.4, 0); // ground drag, none vertically +``` + +`friction` here is per-axis per-step velocity damping, not a surface +coefficient — see the built-in quirks below. + Under matter and planck, `syncFromPhysics()` copies the engine body's position back onto `renderable.pos` after every step, so a direct write is simply erased — no error. The built-in world integrates `pos` in place instead, so a write diff --git a/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md b/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md index 87225033b..616260796 100644 --- a/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md +++ b/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md @@ -129,6 +129,40 @@ Three things matter here: real accessor is `depth` (an alias for `pos.z`); `addChild(child, z)` sets it for you. +### Buttons: extend the handlers, do not bind listeners + +`UISpriteElement` is a `Sprite` that already registers itself for pointer +events, so a button is made by overriding methods rather than by wiring +`registerPointerEvent`: + +| handler | fires | returns | +|---|---|---| +| `onClick(event)` | pressed | `false` to stop the event propagating | +| `onRelease(event)` | pressed and released | `false` to stop propagating | +| `onOver(event)` | pointer enters | — | +| `onOut(event)` | pointer leaves | — | +| `onHold()` | pressed and held | — | + +```js +class MuteButton extends UISpriteElement { + constructor(x, y) { + super(x, y, { image: atlas, region: "speaker.png" }); + this.setOpacity(0.5); + } + onOver() { this.setOpacity(1.0); } + onOut() { this.setOpacity(0.5); } + onClick() { + audio.muteAll(); + return false; // consumed — do not fall through to the world + } +} +``` + +The pointer still has to reach it: a renderable with `isKinematic = true` — the +default on a plain `Renderable` — is skipped by the broadphase and receives +nothing. `UISpriteElement` and `UIBaseElement` clear it for you; anything else +you make clickable has to clear it itself. + ### In a 3D scene, a HUD needs a SMALL depth `floating` opts a renderable out of the camera transform. It does **not** opt it From c2a6e3bd3221127cb5e58f849d52fe07d8b6ea61 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 15 Sep 2026 07:36:19 +0800 Subject: [PATCH 2/2] platformer: order the HUD by depth, not by a property that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `this.z = Number.POSITIVE_INFINITY` on the HUD container and the virtual joypad, with the comment "make sure our object is always draw first". `Renderable` has no `z` property — the accessor is `depth`, an alias for `pos.z` — so both assignments only ever created an expando nothing read, and the two containers were ordered by the world's `autoDepth` instead. It looked right by accident. Both now take their order from an explicit `addChild(child, HUD_Z)` in `play.ts`, which is what the ui skill has been telling readers to do while this example demonstrated the opposite. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t --- packages/examples/src/examples/platformer/entities/HUD.ts | 6 ++++-- .../examples/src/examples/platformer/entities/controls.ts | 6 ++++-- packages/examples/src/examples/platformer/play.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/examples/src/examples/platformer/entities/HUD.ts b/packages/examples/src/examples/platformer/entities/HUD.ts index 4fb735c4b..4347bd4bf 100644 --- a/packages/examples/src/examples/platformer/entities/HUD.ts +++ b/packages/examples/src/examples/platformer/entities/HUD.ts @@ -165,8 +165,10 @@ class UIContainer extends Container { // Use screen coordinates this.floating = true; - // make sure our object is always draw first - this.z = Number.POSITIVE_INFINITY; + // Draw order comes from the `addChild(child, z)` in `play.ts`. + // `renderable.z` is not a property — the accessor is `depth`, an alias + // for `pos.z` — so assigning it here only ever made an expando that + // nothing read, and the container was ordered by `autoDepth` instead. // give a name this.name = "HUD"; diff --git a/packages/examples/src/examples/platformer/entities/controls.ts b/packages/examples/src/examples/platformer/entities/controls.ts index 72baa0ead..588c881d5 100644 --- a/packages/examples/src/examples/platformer/entities/controls.ts +++ b/packages/examples/src/examples/platformer/entities/controls.ts @@ -213,8 +213,10 @@ export class VirtualJoypad extends Container { // Use screen coordinates this.floating = true; - // make sure our object is always draw first - this.z = Number.POSITIVE_INFINITY; + // Draw order comes from the `addChild(child, z)` in `play.ts`. + // `renderable.z` is not a property — the accessor is `depth`, an alias + // for `pos.z` — so assigning it here only ever made an expando that + // nothing read, and the container was ordered by `autoDepth` instead. // give a name this.name = "VirtualJoypad"; diff --git a/packages/examples/src/examples/platformer/play.ts b/packages/examples/src/examples/platformer/play.ts index e27bdbc51..7b6b00d05 100644 --- a/packages/examples/src/examples/platformer/play.ts +++ b/packages/examples/src/examples/platformer/play.ts @@ -17,6 +17,9 @@ import UIContainer from "./entities/HUD"; import { MinimapCamera } from "./entities/minimap"; import { gameState } from "./gameState"; +/** draw order for the screen-space overlays — above every level layer */ +const HUD_Z = 100; + export class PlayScreen extends Stage { private virtualJoypad?: VirtualJoypad; private HUD?: UIContainer; @@ -40,14 +43,15 @@ export class PlayScreen extends Stage { if (typeof this.HUD === "undefined") { this.HUD = new UIContainer(); } - app.world.addChild(this.HUD); + // explicit z: the HUD draws over the level + app.world.addChild(this.HUD, HUD_Z); // display if debugPanel is enabled or on mobile if (plugin.cache.debugPanel?.panel.visible || device.touch) { if (typeof this.virtualJoypad === "undefined") { this.virtualJoypad = new VirtualJoypad(); } - app.world.addChild(this.virtualJoypad); + app.world.addChild(this.virtualJoypad, HUD_Z); } // vignette post-effect + built-in color grading (always applied last).