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

+6
View File
@@ -48,6 +48,11 @@ export interface Project {
* which is also what every collider starts on, so nothing collides differently than it did.
*/
collisionLayers: string[];
/**
* The named render layers (#302), read the same way: which cameras see an object. A project
* written before there were any has just "Default", which every camera sees.
*/
renderLayers: string[];
/**
* Size of the scene view's symbol icons as a factor, 0.1 to 4 (see `symbolScale.ts`). A project
* written before there was one reads as 1, the size the icons always had.
@@ -75,6 +80,7 @@ export function createEmptyProject(id: string, name: string): Project {
startSceneId: '',
plugins: [],
collisionLayers: [DEFAULT_LAYER_NAME],
renderLayers: [DEFAULT_LAYER_NAME],
symbolScale: DEFAULT_SYMBOL_SCALE,
playFrom: DEFAULT_PLAY_FROM,
whilePlaying: DEFAULT_WHILE_PLAYING,
+5 -1
View File
@@ -33,7 +33,11 @@ export interface CollisionFilter {
* is dropped. A project written before there were layers has exactly one, "Default".
*/
export function collisionLayersOf(raw: unknown): string[] {
const list = (raw as { collisionLayers?: unknown } | null)?.collisionLayers;
return layerNamesOf((raw as { collisionLayers?: unknown } | null)?.collisionLayers);
}
/** A list of layer names read off untrusted JSON, by the rules of `collisionLayersOf`. */
export function layerNamesOf(list: unknown): string[] {
const names = (Array.isArray(list) ? list : []).slice(0, MAX_COLLISION_LAYERS);
const read = names.map((name, i) => (typeof name === 'string' && name !== '' ? name : layerFallback(i)));
return read.length > 0 ? read : [DEFAULT_LAYER_NAME];
+52
View File
@@ -0,0 +1,52 @@
/**
* Render layers (#302): which cameras see an object. A drawn object sits on one layer – its own
* choice, else its parent's, at the top `Default` – and a camera shows only the layers it `Sees`.
* They are a list of their own beside the collision layers (#152): what meets and what is seen are
* two questions. The names and masks follow the same rules, 16 at most, so the helpers are shared.
*/
import type { PropertyBag } from './Properties';
import { layerMaskOf, layerNamesOf, MAX_COLLISION_LAYERS, maskHas } from './collisionLayers';
export const RENDER_LAYER_KEY = 'renderLayer';
export const SEES_KEY = 'sees';
export const OVERLAY_KEY = 'overlay';
/** The value of an object that made no choice of its own: it is on its parent's layer. */
export const INHERIT_LAYER = -1;
export const INHERIT_LABEL = 'Inherit';
/** The names as stored; a project written before there were render layers has one, "Default". */
export function renderLayersOf(raw: unknown): string[] {
return layerNamesOf((raw as { renderLayers?: unknown } | null)?.renderLayers);
}
/** An object's own choice: a layer number, or `INHERIT_LAYER` for none – and for anything unreadable. */
export function ownRenderLayer(value: unknown): number {
if (typeof value !== 'number' && !(typeof value === 'string' && value.trim() !== '')) return INHERIT_LAYER;
const n = Math.trunc(Number(value));
return Number.isFinite(n) && n >= 0 && n < MAX_COLLISION_LAYERS ? n : INHERIT_LAYER;
}
/** The layer an object is on: the first own choice going up its parents, layer 0 when none has one. */
export function renderLayerAlong<T>(
item: T,
props: (at: T) => PropertyBag,
parentOf: (at: T) => T | null,
): number {
for (let at: T | null = item; at; at = parentOf(at)) {
const own = ownRenderLayer(props(at)[RENDER_LAYER_KEY]);
if (own !== INHERIT_LAYER) return own;
}
return 0;
}
/** Whether a camera with these properties shows that layer; a camera without `Sees` shows them all. */
export function cameraSees(props: PropertyBag, layer: number): boolean {
return maskHas(layerMaskOf(props[SEES_KEY]), layer);
}
/** Whether a camera draws over the main one instead of being a candidate for it. */
export function isOverlayCamera(props: PropertyBag): boolean {
return props[OVERLAY_KEY] === true;
}