Show each camera only its layers and lay overlay cameras on top (#302)
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 14m26s

A camera could only show the whole scene, so a HUD drawn in the world
would run through a camera shader (#299) with everything else. Render
layers say which cameras see an object, and overlay cameras draw their
own picture over the main one.

- The project has a list of up to 16 render layers, edited in Project
  settings next to the collision layers. It is a list of its own, read
  by the same rules (`layerNamesOf`).
- Sprites, texts, tilemaps, particle sources and containers carry a row
  `Render layer` under their switches. Its default `Inherit` takes the
  parent's layer, at the top the first one.
- A camera has `Sees` (all layers by default) and `Overlay`. The main
  camera is the first one switched on without Overlay, so two plain
  cameras still draw through the first one only.
- Every view through a camera keeps only what that camera sees and lays
  one see-through view per overlay camera over itself. That view is a
  `WorldViewport` of its own, handed its view and frame from below.
- An overlay is fitted into the main camera's whole picture without
  distortion. One of the same size and zoom covers it exactly.
- `z` sorts within one camera only. The pictures lie on top of each
  other as a whole, which is what a shader per camera (#299) needs.
- The Game tab's preview now collects the scene once per redraw. Before
  it collected it twice, with the cameras it would have been five times.
- `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which
  takes the list it sets.

Follow-ups: #304 (the mouse through a chosen camera) and #305 (change
the render layer at run time).

Tests:
- Gate: 226 files, 3487 tests (3480 + 7)
- e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a
  red main sprite at z 1000, in the preview and in the running game.
  A `UI` sprite in front of the main camera and a `Default` one in
  front of the overlay stay away. Without Overlay the second camera
  draws nothing. The settings dialog adds a render layer at the end.
- Counter-checks: inheriting from parents switched off fails 1 unit
  test. Overlays never drawn fails the two overlay checks in e2e 63.
- e2e total 1720 (1716 + 4), computed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 15:32:52 +02:00
1 parent f03ca0d382
commit b2326085ed
33 files changed
+802 -115

No files matched your search

+15 -4
View File
@@ -10,6 +10,7 @@ import { liveTileAt, withTile } from '../core/model/liveTilemap';
import { referenceCandidates } from '../core/model/objectRefs';
import { type Cell, presentTile } from '../core/model/tilemap';
import { descendantsBelow } from '../core/model/walk';
import { isOverlayCamera } from '../core/model/renderLayers';
import { findAsset, findAssetOf, findScene, type Project } from '../core/model/Project';
import { createSceneObject, objectName, type SceneObject } from '../core/model/SceneObject';
import { apply, IDENTITY } from '../core/model/matrix';
@@ -588,12 +589,22 @@ export class World {
/**
* The camera the game is seen through: of the cameras that are switched on (itself and every
* parent, see `properties/enabled.ts`) the first one in hierarchy order. Undefined while none
* is on, and then nothing is rendered – switching one off is how a scene hands the view over.
* parent, see `properties/enabled.ts`) and no overlay (#302) the first one in hierarchy order.
* Undefined while none is on, and then nothing is rendered – switching one off is how a scene
* hands the view over.
*/
activeCamera(): Entity | undefined {
return this.camerasOn().find((camera) => !isOverlayCamera(camera.props));
}
/** The overlay cameras that are switched on (#302), in hierarchy order: the order they are laid on. */
overlayCameras(): Entity[] {
return this.camerasOn().filter((camera) => isOverlayCamera(camera.props));
}
private camerasOn(): Entity[] {
const on = this.entities.filter((e) => e.type === 'camera' && isHandleEnabled(e));
if (on.length < 2) return on[0];
if (on.length < 2) return on;
// Ranked only when the order can decide something, since this runs per frame.
const scene = findScene(this.project, this.sceneId);
const order = scene ? treeOrder(scene).map(({ object }) => object.id) : [];
@@ -601,7 +612,7 @@ export class World {
const index = order.indexOf(camera.objectId);
return index < 0 ? order.length : index;
};
return on.reduce((first, camera) => (rank(camera) < rank(first) ? camera : first));
return on.sort((a, b) => rank(a) - rank(b));
}
/** Removes an entity and, recursively, everything parented to it. */