mcpbeat Sign in

Melonjs Events Agent Skill

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.

1.8k tokens
context cost
SKILL.md, read when the skill is used
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
6 419
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/melonjs/melonJS --skill melonjs-events

The instruction itself

8 sections, as written by the author

Events and lifecycle

The bus

import { event } from "melonjs";

const onUpdate = () => { /* … */ };
const unsubscribe = event.on(event.GAME_UPDATE, onUpdate);  // on() returns its own off()
event.once(event.LEVEL_LOADED, () => { /* … */ });

// two ways to undo it — the same reference, or the returned function
event.off(event.GAME_UPDATE, onUpdate);
unsubscribe();

event.on/once/off/has all take an optional third context argument,

invoked as the handler's this. **off matches on the pair — handler identity

AND context identity.** on(EVT, fn, this) followed by off(EVT, fn) removes

nothing, silently.

Store the handler reference, or use the context argument. An inline arrow or

a fresh .bind(this) in the on call can never be removed — that is the

standard way to leak listeners in melonJS.

// ✗ unremovable
event.on(event.GAME_UPDATE, this.tick.bind(this));

// ✓ keep the bound reference
this._tick = this.tick.bind(this);
event.on(event.GAME_UPDATE, this._tick);
// later: event.off(event.GAME_UPDATE, this._tick);

// ✓ or skip the bind entirely — the context is part of the identity
event.on(event.GAME_UPDATE, this.tick, this);
// later: event.off(event.GAME_UPDATE, this.tick, this);

Custom event names work at runtime (event.emit("my.custom.event", payload)),

but on/emit are typed against a closed map of the engine's own events, so in

TypeScript a name outside that map is a compile error. Use your own emitter for

game events in a .ts project.

The events worth knowing

Per frame

| event | when |

|---|---|

| GAME_BEFORE_UPDATE / GAME_UPDATE / GAME_AFTER_UPDATE | around the world update — GAME_UPDATE is not emitted while paused |

| GAME_BEFORE_DRAW / GAME_AFTER_DRAW | around the draw pass |

| WORLD_STEP | after the physics step |

| TICK | the raw engine tick |

Lifecycle

| event | when |

|---|---|

| BOOT, GAME_INIT, VIDEO_INIT | engine start-up |

| STATE_CHANGE, STATE_PAUSE, STATE_RESUME, STATE_STOP | state machine |

| STAGE_RESET | a stage was reset |

| LEVEL_LOADED | a Tiled level finished loading |

| LOADER_PROGRESS, LOADER_COMPLETE, LOADER_ERROR | asset loading |

| BLUR / FOCUS | the page lost or regained focus |

| CANVAS_ONRESIZE, VIEWPORT_ONRESIZE | relayout hooks |

| POINTERMOVE | global pointer movement |

Use LOADER_PROGRESS / LOADER_COMPLETE. The loader.onProgress /

loader.onload / loader.onError properties were deprecated in 18.2.0 and

removed in 20.3: they were module-level let bindings on an ES module

namespace, so loader.onProgress = fn threw and the documented migration path

never worked. For completion, loader.preload(assets, onloadcb) also takes the

callback directly.

Renderable lifecycle callbacks

Prefer these over bus subscriptions where they fit — the engine calls them at the

right times and there is nothing to unsubscribe:

| callback | when |

|---|---|

| onActivateEvent() | addChild-ed into a container that is already attached to the world root (a Container forwards it to its children) |

| onDeactivateEvent() | removed from its container — including the automatic world clear on a state change |

| onDestroyEvent() | destroy() ran; called with no arguments from the container-removal path |

| onCollisionStart / onCollisionActive / onCollisionEnd | contacts |

Note the activate gate: adding a child to a *detached* container fires nothing.

The callback arrives later, when that container itself is added to the world.

And an isPersistent renderable survives the world clear between stages, so it

gets no fresh onActivateEvent on the next state.

Pair subscriptions with activate/deactivate, not the constructor. This is

the rule that keeps pooled objects working:

onActivateEvent() {
    this._tick = this.update.bind(this);
    event.on(event.GAME_UPDATE, this._tick);
    input.registerPointerEvent("pointerdown", this, this._onDown);
}
onDeactivateEvent() {
    event.off(event.GAME_UPDATE, this._tick);
    input.releasePointerEvent("pointerdown", this);
}

Pool-recycled objects never fire onDestroyEvent on removal — removeChildNow

first tries to return the child to the pool that built it, and only calls

destroy() when nothing owns it. So anything taken from a createPool goes

back to that pool instead. Anything you clean up there will not run. destroy() is also what calls

releaseAllPointerEvents(this), which is why a pooled object must release its

own pointer registrations in onDeactivateEvent.

Guard callbacks that can fire during teardown

Events can arrive while a stage is being dismantled, when the current state is

already something else:

if (!state.isCurrent(state.PLAY)) return;

Without this, a "children emptied" handler can reset the wrong stage.

Removing objects from a collision callback

Contact callbacks fire inline inside the physics step, mid-iteration over the

detector's pair list. container.removeChild() is safe there — it defers the

actual removal to the next tick, which is exactly why it defers. The immediate

forms are the dangerous ones: removeChildNow() tears the object down under

the iteration, via the internal destroy() it calls, which nulls pos and

body. (destroy() is engine teardown and is not in the published types, so

removeChildNow is the call a game actually makes.) The builtin

detector guards the pair it is currently resolving, but nothing guarantees that

for the rest of the step or for another adapter.

Use removeChild(), or flag the work and drain it from Stage.update.

Symptom → cause

| symptom | cause |

|---|---|

| handler still fires after removing the object | event.off given a different function reference, or a different context than on got |

| handlers accumulate on pooled objects | subscribed in the constructor rather than onActivateEvent |

| cleanup never runs on a pooled object | pooled removal does not fire onDestroyEvent |

| crash during a stage switch | callback ran mid-teardown — guard with state.isCurrent |

| crash after a collision | removeChildNow() inside a contact callback — use removeChild() |

| progress callbacks never fire | assigned loader.onProgress (removed in 20.3) instead of subscribing to LOADER_PROGRESS |

| onActivateEvent never runs | the child was added to a container not yet attached to the world |

  • melonjs-scenes-and-state — the state machine and stage lifecycle
  • melonjs-performance — pooling, and why activate/deactivate pairing matters

How to use it

Copy the folder

Take melonjs/melonjs-events from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.