-
-
Notifications
You must be signed in to change notification settings - Fork 659
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
- What is the difference between video.init() and new Application()?
- 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",
});
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:
- Cordova — wraps your game in a native mobile app shell for iOS and Android.
- Capacitor — a modern alternative to Cordova by the Ionic team.
- 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.
new Application(width, height, options) is the recommended way to start a melonJS game since version 18.3. It creates the renderer, game world, viewport, and starts the game loop in a single call.
import { Application } from "melonjs";
const app = new Application(800, 600, {
parent: "screen",
scale: "auto",
});video.init() is the legacy entry point — it still works but is deprecated. It internally calls Application.init() on the default game instance.
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-detect: tries WebGL first, falls back to Canvas
const app = new Application(800, 600, { renderer: video.AUTO });
// Force Canvas 2D
const app = new Application(800, 600, { renderer: video.CANVAS });
// Force WebGL
const app = new Application(800, 600, { renderer: video.WEBGL });The rendering API is identical on both backends — your game code doesn't change. WebGL offers better performance for most games (GPU-accelerated batching), while Canvas is a reliable fallback. See the Rendering API page for the full API reference.
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
});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);
});