Skip to content

Add multi-camera support with minimap example#1310

Merged
obiot merged 2 commits intomasterfrom
feat/multi-camera
Mar 25, 2026
Merged

Add multi-camera support with minimap example#1310
obiot merged 2 commits intomasterfrom
feat/multi-camera

Conversation

@obiot
Copy link
Copy Markdown
Member

@obiot obiot commented Mar 25, 2026

Summary

  • Add proper multi-camera support to Camera2d (screenX, screenY, zoom, autoResize, isDefault, worldView, setViewport(), worldProjection, screenProjection)
  • Add visibleInAllCameras to Renderable for per-camera floating element filtering
  • Implement setProjection() in CanvasRenderer (ortho matrix → canvas 2D transform)
  • Auto-flush in WebGL setProjection() to prevent batched quads rendering with wrong projection
  • Add minimap camera example to platformer with viewport highlight and player marker
  • 95+ new unit tests covering all new APIs

Test plan

  • All 1511 tests pass
  • Minimap renders correctly in WebGL mode (tiles, sprites, backgrounds, clouds)
  • Minimap renders correctly in Canvas mode
  • UI/HUD elements only appear on default camera
  • Browser resize doesn't break display
  • Viewport wider than level doesn't offset minimap
  • Single-camera performance path unchanged

🤖 Generated with Claude Code

Add screenX, screenY, zoom, autoResize, isDefault, worldView, setViewport,
worldProjection and screenProjection to Camera2d for proper multi-camera
rendering. Add visibleInAllCameras to Renderable for per-camera floating
element filtering. Implement setProjection in CanvasRenderer to properly
apply ortho projection as canvas 2D transform. Auto-flush in WebGL
setProjection to prevent batched quads from rendering with wrong projection.
Add minimap camera example to platformer with viewport highlight and player
marker. Includes 95+ new unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 11:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces first-class multi-camera support in Camera2d (viewport placement + zoom) and updates rendering logic so world/floating elements can be filtered per camera, with an updated platformer example demonstrating a minimap camera.

Changes:

  • Extend Camera2d with screenX/screenY, zoom, autoResize, isDefault, worldView, setViewport(), and per-camera projection matrices.
  • Add visibleInAllCameras to Renderable and update Container/ImageLayer rendering to support per-camera floating element behavior.
  • Implement Canvas/WebGL projection switching behavior and add a minimap camera example; add/extend unit tests for new APIs.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/melonjs/src/camera/camera2d.ts Adds multi-camera API surface (viewport offset, zoom, projections) and updates visibility + draw pipeline.
packages/melonjs/src/renderable/container.js Skips UI-only floating elements on non-default cameras; applies screen/world projections for floating draws.
packages/melonjs/src/renderable/renderable.js Adds visibleInAllCameras flag used for multi-camera floating filtering.
packages/melonjs/src/renderable/imagelayer.js Ensures background layers render in all cameras and compute parallax based on the active viewport (incl. zoom).
packages/melonjs/src/video/canvas/canvas_renderer.js Applies projection matrices to Canvas via a 2D transform.
packages/melonjs/src/video/webgl/webgl_renderer.js Flushes batches before switching projection matrices to avoid incorrect batched rendering.
packages/melonjs/tests/camera.spec.js Expands camera coverage for zoom, projections, viewport offsets, and draw state restoration.
packages/melonjs/tests/renderable.spec.js Adds tests for visibleInAllCameras.
packages/melonjs/tests/container.spec.js Adds tests around floating renderables and multi-camera visibility defaults.
packages/melonjs/tests/bounds.spec.ts Adds tests for Bounds.setMinMax.
packages/melonjs/CHANGELOG.md Notes multi-camera support and Canvas projection behavior change.
packages/examples/src/examples/platformer/play.ts Adds a minimap camera to the platformer stage.
packages/examples/src/examples/platformer/entities/minimap.ts New minimap camera implementation + overlays.
packages/examples/src/examples/platformer/createGame.ts Switches example renderer selection to video.AUTO.
Comments suppressed due to low confidence (1)

packages/melonjs/src/camera/camera2d.ts:922

  • For non-default cameras, drawFX() is called while the renderer projection is still worldProjection (and in Canvas mode drawFX() resets the transform to identity). This causes fade/flash overlays to render at the wrong scale/position (often not covering the camera viewport when screenX/screenY or zoom are set). Consider switching to screenProjection (or drawing in width/zoom world units) around drawFX() for non-default cameras, and ensure Canvas mode reapplies the projection after resetTransform().
		// draw the viewport/camera effects
		this.drawFX(renderer);


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +19
// add a second camera
this.cameras.set("minimap", new MinimapCamera());
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a new MinimapCamera() unconditionally in onResetEvent can leak/accumulate cameras across state transitions: Stage.reset() does not clear this.cameras, and Stage.destroy() only clears the map without destroying cameras. Consider guarding against duplicates (e.g. reuse an existing minimap camera if already present) or explicitly removing/destroying the previous instance before overwriting the key.

Suggested change
// add a second camera
this.cameras.set("minimap", new MinimapCamera());
// add a second camera (reuse if it already exists)
let minimapCamera = this.cameras.get("minimap") as MinimapCamera | undefined;
if (!minimapCamera) {
minimapCamera = new MinimapCamera();
this.cameras.set("minimap", minimapCamera);
}

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +23
// reposition on canvas resize (keep anchored to top-right)
event.on(event.CANVAS_ONRESIZE, (w: number) => {
this.screenX = w - MINIMAP_WIDTH - 10;
});
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This event.on(CANVAS_ONRESIZE, ...) subscription is never removed. Since stages/cameras can be reset multiple times, this can leave stale listeners updating orphaned cameras. Consider storing the listener function and calling event.off(...) in an onDestroyEvent()/destroy() override (or otherwise ensuring only one listener is registered).

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +46
renderer.setColor("#ffffff");
renderer.lineWidth = 1.5 * screenPx;
renderer.strokeRect(view.left, view.top, view.width, view.height);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderer.lineWidth is modified here, but in WebGL the renderer save/restore stack does not track lineWidth, so this value can leak into subsequent draws even after super.postDraw() restores. Consider saving the previous lineWidth and restoring it before returning (or explicitly resetting it after drawing the minimap overlays).

Copilot uses AI. Check for mistakes.
this.screenY = y;
if (typeof w !== "undefined" && typeof h !== "undefined") {
super.resize(w, h);
this._updateProjectionMatrix();
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setViewport() resizes the camera rect via super.resize(w, h) but does not update size-dependent state like the deadzone (used by follow logic). After changing w/h, the existing deadzone may no longer fit the camera and can cause incorrect follow behavior. Consider updating the deadzone (and any other size-derived values) when w/h are provided, without resetting bounds like resize() does.

Suggested change
this._updateProjectionMatrix();
this._updateProjectionMatrix();
// keep size-dependent state (like the follow deadzone) consistent
// with the new viewport size without resetting camera bounds
if (this.deadzone != null) {
// ensure the deadzone rectangle still fits inside the camera
const dz = this.deadzone;
if (dz.width > this.width || dz.height > this.height) {
// shrink the deadzone to at most the camera size, preserving its origin
dz.width = Math.min(dz.width, this.width);
dz.height = Math.min(dz.height, this.height);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +349 to +353
this._worldView.setMinMax(
this.pos.x,
this.pos.y,
this.pos.x + this.width / this.zoom,
this.pos.y + this.height / this.zoom,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worldView (and the non-default camera projection math) divides by this.zoom, but zoom is a plain public number with no validation. Setting zoom to 0 (or a negative value) will yield infinities/NaNs in worldView and ortho() and can break rendering and visibility checks. Consider adding a getter/setter for zoom that clamps to a small positive value or throws on invalid input.

Suggested change
this._worldView.setMinMax(
this.pos.x,
this.pos.y,
this.pos.x + this.width / this.zoom,
this.pos.y + this.height / this.zoom,
// Guard against invalid zoom values (0, NaN, Infinity) to avoid
// infinities/NaNs in the computed bounds.
let safeZoom = this.zoom;
if (!Number.isFinite(safeZoom) || safeZoom === 0) {
safeZoom = 1;
}
this._worldView.setMinMax(
this.pos.x,
this.pos.y,
this.pos.x + this.width / safeZoom,
this.pos.y + this.height / safeZoom,

Copilot uses AI. Check for mistakes.
- Guard against duplicate minimap camera in onResetEvent
- Store and cleanup CANVAS_ONRESIZE listener in minimap destroy()
- Save/restore lineWidth in minimap postDraw
- Validate zoom setter to reject zero/negative values (fall back to 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obiot obiot merged commit 87f3404 into master Mar 25, 2026
6 checks passed
@obiot obiot deleted the feat/multi-camera branch April 11, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants