-
-
Notifications
You must be signed in to change notification settings - Fork 662
Frequently Asked Questions
- How to get started with melonJS
- How to optimize your game for performance
- How to package your game for desktop or mobile
- How to access object properties from another object
- How to use the object pooling feature
- Handling pointer events on overlapping objects
- Using form inputs in your game
- Resolving collisions properly
- How do I start a melonJS game? (and what happened to video.init()?)
- How to use WebGL vs Canvas renderer
- How to handle screen resize and scaling
- How to debug my game
- How to load and switch between levels
The fastest way to create a new game:
npm create melonjs my-game
cd my-game
npm install
npm run devThis scaffolds a ready-to-run project with TypeScript, Vite, and the debug plugin. You can also start from the boilerplate or follow the Platformer Tutorial.
For a minimal setup without scaffolding:
import { Application, Text } from "melonjs";
const app = new Application(800, 600, {
parent: "screen",
scale: "auto",
});
// mandatory since 20.0 — builds the renderer and appends the canvas
await app.init();
app.world.addChild(new Text(400, 300, {
font: "Arial",
size: 48,
textAlign: "center",
text: "Hello World!",
}));- Use Object Pooling to reduce memory usage by reusing objects and lowering garbage collection pressure.
- Use a Texture Atlas to batch draw calls. Unless you target WebGL1 only, texture sizes don't need to be power-of-two, but keeping them reasonable helps GPU memory.
- Avoid using
Textextensively during gameplay — preferBitmapText, especially when using WebGL. - The game resolution directly impacts performance. Try lowering the resolution in your Application constructor.
- If you use TexturePacker, disable texture rotation, as it forces extra rotation calculations when drawing sprites.
- Use PNG images with alpha channel instead of the "transparent color" setting in Tiled tilesets.
- For tile layers with very few tiles, avoid pre-rendering — sparse layers draw faster dynamically.
- Use radians directly for rotation angles instead of converting from degrees. A full turn is
Math.PI * 2, a half turn isMath.PI, a quarter turn isMath.PI * 0.5. - Access object properties directly (
this.last_animation === "walk") instead of calling methods (this.isCurrentAnimation("walk")) when performance matters. - Try a lower fps rate — does your game really require 60fps?
- Use the Chrome DevTools Performance profiler to locate bottlenecks.
Desktop:
- Electron — a popular Chromium-based wrapper for packaging web apps as native desktop applications (Windows, Mac, Linux).
- Tauri — a modern, lightweight alternative to Electron using the system's native webview.
- NW.js — a similar alternative, originally known as Node-Webkit.
Mobile:
-
Capacitor — modern native runtime by the Ionic team; recommended for new projects, with melonJS-specific glue via
@melonjs/capacitor-plugin. - Cordova — Apache's original WebView-wrapper; still maintained but in low-maintenance mode (use Capacitor for new projects).
- Progressive Web App (PWA) — add a manifest and service worker to make your game installable from the browser.
Web:
- Facebook Instant Games — deploy directly to the Facebook gaming platform.
- WeChat Mini Games — deploy to WeChat's mini game platform.
- Get a reference to the target object using
app.world.getChildByName():
// In your Stage's onResetEvent(app)
const player = app.world.getChildByName("player")[0];
// Then use it anywhere
const playerPos = player.pos;
player.doSomething();-
getChildByName()returns an array — if you have one player object, it will be the first element. - Call it once and store the reference — don't call it every frame.
Object Pooling reduces the overhead of frequently creating and destroying objects (like bullets or particles). Instead of creating new instances each time, objects are recycled from a pool.
First, register your class with the pool (do this once, e.g. in your game setup):
import { pool } from 'melonjs';
pool.register("laser", LaserEntity, true);The third argument true enables recycling. Your class must implement onResetEvent() to properly reinitialize its state.
Then create instances using pool.pull() instead of new:
// Pull from pool instead of new LaserEntity(...)
const myLaser = pool.pull("laser", this.pos.x, this.pos.y);
app.world.addChild(myLaser, 3);When the object is removed from the world, it's automatically returned to the pool for reuse.
Note: classes registered with
pool.register()are also automatically available as Tiled object factories — objects placed in a Tiled map with a matching class or name will be instantiated using the registered constructor.
When multiple objects overlap on screen, pointer events can conflict. A common example is registering events on the viewport rect from multiple objects.
The recommended approach is to register the event in a single top-level object and delegate using the event system:
import { input, event } from 'melonjs';
// Register once in a top-level object (e.g. in onResetEvent(app))
input.registerPointerEvent("pointerdown", app.viewport, (e) => {
event.emit("pointerdown", e);
});
// Listen from any object
event.on("pointerdown", this.onPointerDown, this);
// Clean up when done
event.off("pointerdown", this.onPointerDown, this);You can overlay native HTML form elements on top of the melonJS canvas. This gives you built-in keyboard handling, focus management, copy/paste, and mobile virtual keyboard support.
class TextInput extends Renderable {
constructor(x, y, type, maxLength) {
super(x, y, maxLength * 10, 20);
this.input = document.createElement("input");
this.input.type = type;
this.input.style.position = "absolute";
this.input.style.left = `${x}px`;
this.input.style.top = `${y}px`;
this.input.style.zIndex = "2";
if (type === "text") {
this.input.maxLength = maxLength;
}
this.parentApp.getParentElement().appendChild(this.input);
}
getValue() {
return this.input.value;
}
destroy() {
this.input.remove();
super.destroy();
}
}Position the parent element with position: relative so the inputs align with the canvas:
#screen {
position: relative;
}melonJS uses a combination of QuadTree spatial indexing, AABB tests, and SAT (Separating Axis Theorem) for collision detection — giving both accuracy and performance.
Collision detection vs. collision response:
Detection tells you if two objects overlap. Response determines what happens — does the object stop, bounce, pass through, trigger an event?
In melonJS, collision responses are handled in the onCollision callback:
onCollision(response, other) {
// return true to apply the collision response (solid)
// return false to ignore it (pass-through, trigger only)
if (other.body.collisionType === collision.types.ENEMY_OBJECT) {
this.takeDamage();
return false; // don't stop movement
}
return true; // solid collision
}Common gotchas:
-
Fast-moving objects can pass through thin walls if their velocity exceeds the wall thickness in a single frame. Consider using thicker collision shapes or limiting maximum velocity.
-
Corner collisions can cause unexpected "bumping" perpendicular to momentum. This happens because SAT uses the shortest overlap depth as the response vector, which may not align with the direction of movement.
-
Internal edges between adjacent collision shapes can cause objects to get stuck. Use larger, merged collision shapes when possible instead of many small adjacent ones.
Starting a game is two steps: construct the Application, then await its init().
import { Application } from "melonjs";
const app = new Application(800, 600, {
parent: "screen",
scale: "auto",
});
// mandatory since 20.0 — builds the renderer, appends the canvas,
// and starts the game loop
await app.init();Why is it asynchronous? A WebGPU device cannot be acquired synchronously, and WebGPU is the first backend video.AUTO negotiates since 20.0. init() still resolves without suspending on the WebGL and Canvas backends. Two consequences worth knowing:
- Renderer failures now surface as a rejection of
init()rather than a constructor throw —catchit if you want to show a "your browser is not supported" message. -
app.destroy()is terminal: a destroyed application cannot be re-initialized (construct a new one instead), and callinginit()twice on a live app is a warned no-op.
What about video.init()? It was the legacy entry point and is removed in 20.0 (deprecated since 18.3). Replace it with the two-step form above; note the failure mode differs — video.init() returned false when the canvas could not be created, whereas app.init() rejects. video.renderer is gone too: use app.renderer.
Key benefits of Application:
-
Explicit ownership —
app.renderer,app.world,app.viewportare all on the instance -
Clean lifecycle —
app.destroy()properly cleans up for SPA/React environments -
Stage integration —
onResetEvent(app)andonDestroyEvent(app)receive the app instance -
No globals needed — access the app from any renderable via
this.parentApp
import { Application, video } from "melonjs";
// Auto-negotiate the best available backend: WebGPU → WebGL 2 → Canvas
const app = new Application(800, 600, { renderer: video.AUTO });
await app.init();
// Or force one — init() rejects if it is unavailable (never substitutes)
new Application(800, 600, { renderer: video.WEBGPU });
new Application(800, 600, { renderer: video.WEBGL });
new Application(800, 600, { renderer: video.CANVAS });The rendering API is identical on all three backends — your game code doesn't change. Since 20.0 the default video.AUTO prefers WebGPU, then WebGL 2, then Canvas; the two GPU backends render the entire engine feature set identically (sprites, shader effects, lights, meshes, Camera3d, GPU tilemaps), while Canvas remains the universally-available fallback without the GPU-only features. You can also force a backend for a single run with the #webgpu / #webgl / #canvas URI fragment.
Code that needs to know what it actually got should read the capability flags (renderer.type, renderer.shaderLanguage, renderer.supportsDepthBuffer, …) rather than sniff the class. See the Rendering API page for the full API reference and the flag table.
melonJS handles scaling automatically. Set the scale and scaleMethod options:
const app = new Application(800, 600, {
parent: "screen",
scale: "auto", // auto-scale to fit the parent element
scaleMethod: "flex-width" // stretch width, keep height
});
await app.init();Available scaleMethod values:
-
"fit"— scale to fit inside the parent, maintaining aspect ratio (letterboxed) -
"fill-min"/"fill-max"— scale to fill the parent, cropping excess -
"flex"/"flex-width"/"flex-height"— stretch one or both axes to fill -
"stretch"— stretch to fill without maintaining aspect ratio
Install the official debug plugin:
npm install @melonjs/debug-pluginimport { DebugPanelPlugin } from "@melonjs/debug-plugin";
import { plugin } from "melonjs";
plugin.register(DebugPanelPlugin, "debugPanel");Press S to toggle the debug panel, which shows:
- FPS and frame timing
- Object count and draw calls
- Collision shapes and bounding boxes
- Velocity vectors
- QuadTree visualization
You can also use Chrome DevTools Performance profiler, and console.log within update() or draw() methods.
Use Tiled to create your levels, then load them in your Stage:
import { level, Stage } from "melonjs";
class PlayScreen extends Stage {
onResetEvent(app) {
level.load("map1");
}
}To switch levels:
// Switch to a different state (which loads a different level)
state.change(state.PLAY, { level: "map2" });
// Or reload the current level
level.reload();Assets (images, TMX maps, audio) must be preloaded before use:
import { loader } from "melonjs";
loader.preload(resources, () => {
// assets are ready, start the game
state.change(state.PLAY);
});