Use this skill when adding a melonJS plugin or adapter — the debug panel, Spine skeletal animation, compressed Tiled map support, on-screen touch controls, Capacitor packaging — or when writing your o
1---2name: melonjs-plugins3description: Use this skill when adding a melonJS plugin or adapter — the debug panel, Spine skeletal animation, compressed…4---56# packages/melonjs/skills/melonjs-plugins/SKILL.md
M
SKILL.mdAgent skill
Transport
File
Status
Listed
SKILL.md
Install
The skill file stays in the publisher's repository.
Paste the copied command in a terminal. It installs this SKILL.md into that agent.
shterminal
1npx skills add melonjs/melonJS --skill melonjs-plugins -a cursor -y
Paste the copied command in a terminal. It installs this SKILL.md into that agent.
Use this skill when adding a melonJS plugin or adapter — the debug panel, Spine skeletal animation, compressed Tiled map support, on-screen touch controls, Capacitor packaging — or when writing your own plugin. Covers plugin.register and its version gate, plugin.cache, plugin.patch, and why physics adapters are not plugins. Triggers on: plugin, plugin.register, plugin.cache, plugin.get, plugin.patch, BasePlugin, debug-plugin, DebugPanelPlugin, SpinePlugin, TiledInflatePlugin, GamepadPlugin, gamepad-plugin, touch controls, virtual joystick, floating joystick, floating button, CapacitorPlugin, adapter, custom plugin, plugin version mismatch, already registered.
Download packages/melonjs/skills/melonjs-plugins/SKILL.md from the publisher, or copy the path and open the repository. PlugMCP lists the skill and does not host the file.
Where does melonjs-plugins live?
The publisher keeps it at https://github.com/melonjs/melonJS.
Which agents can use melonjs-plugins?
Claude, Cursor, Codex, and Grok Build can read a SKILL.md you add from the publisher's repository. Grok Bot installs it when you send the skill's GitHub folder link in a chat.
From the publisher
SKILL.md
name
melonjs-plugins
description
Use this skill when adding a melonJS plugin or adapter — the debug panel, Spine skeletal animation, compressed Tiled map support, on-screen touch controls, Capacitor packaging — or when writing your own plugin. Covers plugin.register and its version gate, plugin.cache, plugin.patch, and why physics adapters are not plugins. Triggers on: plugin, plugin.register, plugin.cache, plugin.get, plugin.patch, BasePlugin, debug-plugin, DebugPanelPlugin, SpinePlugin, TiledInflatePlugin, GamepadPlugin, gamepad-plugin, touch controls, virtual joystick, floating joystick, floating button, CapacitorPlugin, adapter, custom plugin, plugin version mismatch, already registered.
license
MIT
Plugins and adapters
Two different mechanisms
This trips people up, so it is worth stating first:
how it is installed
Plugins
plugin.register(Class, name?, ...args) — after await app.init()
Physics adapters
an Applicationsetting (physic:), before init()
// the adapter is chosen at construction …
const app = new Application(800, 600, {
parent: "screen",
physic: new PlanckAdapter({ gravity, pixelsPerMeter }),
});
// … the plugin is registered once the renderer exists
await app.init();
plugin.register(DebugPanelPlugin, "debugPanel");
register in detail
plugin.register(PluginClass, "name", ...constructorArgs);
plugin.cache.name; // the instance
plugin.get(PluginClass); // by class — or by registered name string
The name is optional — it falls back to the class name.
The registry is global, not per-application. It is a module-level object,
so registering the same name twice throws plugin <name> already registered.
A plugin instance does hold an app reference, but it defaults to game —
whichever Application most recently finished init(), since that is where
game is assigned. On a multi-application page, pass the app explicitly
through the constructor args:
plugin.register(DebugPanelPlugin, "debugPanel", input.KEY.S, myApp).
BasePlugin.version is a floor, not an identity. It declares the
minimum melonJS version the plugin needs, and register throws
Plugin version mismatch, expected: <floor>, got: <engine> when the running
engine is older. That gate is the real enforcement — a peer-dependency range
only warns at install time, and not at all for CDN or pre-bundled use. If you
see that error, upgrade the engine or pin an older plugin. The check runs
the instance is constructed, so a plugin whose constructor has side
effects will already have run them when it throws.
after
Register after await app.init(): a plugin binds to an Application and reads
renderer and frame state that does not exist before init.
The official plugins
@melonjs/debug-plugin
An in-game panel — frame time, draw and object counts — plus toggles for
hitbox/bounding-box overlays, velocity vectors and quadtree visualisation. The
first thing to reach for when something is slow or a collision is not landing
where you expect.
import { DebugPanelPlugin } from "@melonjs/debug-plugin";
plugin.register(DebugPanelPlugin, "debugPanel"); // default toggle key: S
@melonjs/tiled-inflate-plugin
Required, not optional, for Tiled maps using gzip, zlib or zstd layer
compression. It installs the inflate hook TMXUtils calls; without it a
compressed .tmx fails to parse — a map that opens fine in Tiled and refuses to
load in melonJS is nearly always this.
import { TiledInflatePlugin } from "@melonjs/tiled-inflate-plugin";
plugin.register(TiledInflatePlugin);
@melonjs/spine-plugin
Spine 4.x skeletal animation as a melonJS renderable, on all three backends.
This is the answer for deforming animated characters — glTF import does not read
vertex skinning. It also registers a Tiled object class, so Spine skeletons can
be placed from a map.
@melonjs/gamepad-plugin
On-screen touch controls, reported as a gamepad: one or two sticks and any
number of buttons, drawn over the game and read by the engine as a pad in a
gamepad slot. The game's bindGamepad bindings drive it unchanged, so one set
of bindings covers a real pad and a touch screen.
import { GamepadPlugin } from "@melonjs/gamepad-plugin";
plugin.register(GamepadPlugin, "gamepad", {
buttons: [{ label: "A" }, { label: "B" }],
});
// plugin.cache.gamepad.visible — showing, to word prompts ("tap" or a key)
Shown on touch screens by default (enabled), hidden while a real pad is in
use. The left stick also presses the D-pad; a button can press a key instead
of a pad button. Since 1.1 a stick or button can be floating (shows up
under the finger anywhere in its area), hideAtRest (drawn only while
held), and a stick can take a tap (a quick touch that does not steer presses
a key or button) — so the mobile layouts, two thumbs or one thumb with
tap-to-act, need no code of their own. See melonjs-input for the bindings,
the layouts and the overlap rules.
@melonjs/capacitor-plugin
Bridges Capacitor's native lifecycle and hardware-back events into the engine:
import { CapacitorPlugin } from "@melonjs/capacitor-plugin";
plugin.register(CapacitorPlugin, "capacitor", options);
// plugin.cache.capacitor.teardown() — detach every listener
It also exports helpers usable on their own: onBackButton, bindStageBack,
lockOrientation / unlockOrientation, hideSplash. See melonjs-deployment
for the wider packaging picture.
With pauseOnBackground (the default), going to the background pauses the
game and coming back resumes it, but only if this pause was the plugin's: a
game the player had paused stays paused.
Physics adapters
@melonjs/planck-adapter and @melonjs/matter-adapter swap the physics world
wholesale. Pick one per game. The portable surface — the declarative
renderable.bodyDef, world.adapter.queryAABB(rect) and
world.adapter.raycast(from, to) — works unchanged across the built-in world and
both adapters, so entity code does not have to branch.
Two queries are not portable: querySphere and raycast3d exist only on the
built-in adapter (planck and matter are 2D-only and deliberately omit them).
queryAABB is the one region query every adapter must implement, so call the
others optionally — app.world.adapter.querySphere?.(center, radius). For rays
there is a real flag: app.world.adapter.capabilities.raycasts3d.
Behaviour genuinely differs: velocity units, force magnitudes and where a body's
position is measured from all change. See melonjs-physics.
Writing your own
There is no init() lifecycle hook — the constructor is where setup goes,
and register instantiates it for you.
import { plugin, event } from "melonjs";
class MyPlugin extends plugin.BasePlugin {
constructor(app = undefined) {
super(app); // stores it as this.app
this.version = "20.0.0"; // MINIMUM engine version this needs
event.on(event.GAME_UPDATE, () => { /* … */ });
}
}
plugin.register(MyPlugin, "myPlugin");
this.version defaults to the running engine version, which means it always
passes — set it deliberately to the oldest version you actually support.
To wrap an existing engine method, plugin.patch keeps the original reachable
as this._patched():
plugin.patch(app, "update", function (time) {
const result = this._patched(time);
return result;
});
patch takes a class or an instance: given a constructor it patches the
prototype (that is how the debug plugin decorates Renderable.postDraw), given a
plain object it patches that object. this._patched is only bound for the
synchronous duration of your replacement — it is set before the call and nulled
straight after — so call it inline, never after an await.
plugin.patch(Renderable, "postDraw", function (renderer) {
this._patched(renderer);
// draw an overlay on top of every renderable
});
A plugin is the right shape when you need to augment engine classes or hook the
engine lifecycle globally. For anything that is just game code, a Renderable
or a Stage is simpler and easier to test.
Symptom → cause
symptom
cause
plugin <name> already registered
the registry is global; registered twice, often across hot reloads
Plugin version mismatch
the plugin's version floor is above the running engine
Plugin should extend the BasePlugin Class !
the class does not extend plugin.BasePlugin
the panel reports another app's numbers
app defaulted to game, the last Application to finish init()
world.adapter.querySphere is not a function
built-in adapter only; planck and matter are 2D-only
a Tiled map opens in Tiled but not melonJS
compressed layers, tiled-inflate plugin missing
plugin setup never runs
setup written in an init() method — there is no such hook
plugin.register for an adapter does nothing
adapters are a construction setting, not a plugin
physics behaves differently after adding an adapter
expected — units and force scales differ; re-tune
a Spine object slides off screen when the camera moves, or ignores its container (WebGL / WebGPU)
spine-plugin < 4.0.1 drew without the camera's and the parents' transform; upgrade
a Spine object ignores setOpacity() / alpha
spine-plugin < 4.0.1; upgrade
a game paused from its menu is running again after the app comes back from the background
capacitor-plugin < 1.0.1 resumed on every return; upgrade
touch controls miss the finger after browser zoom or a move to another screen
gamepad-plugin < 1.2.1; upgrade
Related skills
melonjs-physics — the adapters and their behavioural differences
melonjs-tilemaps — where the inflate plugin becomes mandatory
melonjs-performance — using the debug panel to measure
melonjs-deployment — Capacitor and native packaging
melonjs-3d-assets — why skeletal characters go through Spine