Add a node that names the render layer of an object (#305)
game-maker / gate (push) Successful in 3m14s
game-maker / suite (push) Failing after 14m18s

The issue asked for Set render layer, Set sees and Render layer of. As
with #211, a probe showed that writing already works. A Properties
node of the object's type writes Render layer during a run, Inherit
included, and one of type Camera writes Sees. The next picture follows
both. As agreed, only Render layer of is built, and writing stays with
Properties.

- Render layer of is a data node with the input object and the outputs
  name and number. It answers the layer that holds, so an object
  without a choice of its own names its parent's.
- NodeContext.renderLayerOf answers the number with the same rule the
  drawing uses (renderLayerAlong). The new field renderLayers on
  GraphLookup brings the names to the node.
- hasRenderLayer in PropertySchema says which types are drawn.
- Decided here: for an object that is not drawn (a camera, a
  behavior, a collider) the name is empty and the number -1, as Layer
  of does without a collider.

Changing the render layer or Sees during a run had no test so far.
Three unit tests now hold the proof from the issue.

Tests:
- Gate: 231 files, 3534 tests (3526 + 8), exit 0
- e2e 6: 149 checks passed, e2e 63: 13 checks passed (+1)
- Unit: the coin moves into the overlay with Set and back with
  Inherit, a HUD container takes its sprite along, the main camera sees
  a second layer once its Sees is set, and Render layer of names the
  inherited layer, and none for a behavior
- Node catalog: four cases for Render layer of
- 63c3 moves a sprite onto UI in a Play run: the main camera no longer
  draws it, and the log reads "UI" and "1"
- Three counter-probes fail 1, 1 and 2 tests: without inheriting in
  the node, without the check for a drawn type, and without
  inheriting in the drawing
- "layer of" in the palette still finds Layer of first (6z-x)
- The e2e sum is 1731 (1730 plus 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 19:25:47 +02:00
1 parent 70f3608733
commit d9c6bbfa4d
12 files changed
+250 -8

No files matched your search

+22
View File
@@ -609,6 +609,28 @@ const CASES: Record<string, Case[]> = {
},
{ name: 'no object is on no layer', inputs: { object: null }, outputs: { name: '', number: -1 } },
],
RenderLayerOf: [
{
name: 'names the render layer the context says the object is drawn on (#305)',
inputs: { object: fakeEntity('Coin') },
context: { renderLayerOf: (target) => (target.name === 'Coin' ? 1 : null) },
lookup: { ...NO_LOOKUP, renderLayers: () => ['Default', 'UI'] },
outputs: { name: 'UI', number: 1 },
},
{
name: 'a render layer the project has not named goes by its number',
inputs: { object: fakeEntity('Coin') },
context: { renderLayerOf: () => 3 },
lookup: { ...NO_LOOKUP, renderLayers: () => ['Default'] },
outputs: { name: 'Layer 3', number: 3 },
},
{
name: 'an object that is not drawn is on no render layer',
inputs: { object: fakeEntity('Cam') },
outputs: { name: '', number: -1 },
},
{ name: 'no object is on no render layer', inputs: { object: null }, outputs: { name: '', number: -1 } },
],
ObjectsByName: [
{
name: 'every object so named, in scene order',
+1
View File
@@ -244,6 +244,7 @@ export function fakeContext(overrides: Partial<NodeContext> = {}): FakeContext {
raycast: () => null,
raycastAll: () => [],
collisionLayerOf: () => null,
renderLayerOf: () => null,
ignoreOneWay: () => {},
isTilemap: () => false,
tileAt: () => null,
+124 -3
View File
@@ -1,7 +1,10 @@
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
import { SetLayerNamesCommand } from '../../src/core/commands/projectCommands';
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { PlaceAssetCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import type { GraphNode } from '../../src/core/graph';
import { OBJECT_TYPE_PARAM } from '../../src/core/graph/propertyPorts';
import type { PropertyBag } from '../../src/core/model/Properties';
import {
cameraSees,
INHERIT_LAYER,
@@ -13,10 +16,15 @@ import {
} from '../../src/core/model/renderLayers';
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
import { settingsChanges } from '../../src/editor/ProjectSettingsDialog';
import { cameraOfItems, overlaysOfItems, overlaysOfWorld } from '../../src/viewport/activeCamera';
import {
cameraOfItems,
cameraOfWorld,
overlaysOfItems,
overlaysOfWorld,
} from '../../src/viewport/activeCamera';
import { collectHolderItems, collectWorldItems } from '../../src/viewport/items';
import { cameraView, overlayView } from '../../src/viewport/view';
import { makeSpriteScene, place, startWorld } from '../helpers';
import { type GraphEditor, graphEditor, makeBehavior, makeSpriteScene, place, startWorld } from '../helpers';
function sceneWithCamera() {
const fixture = makeSpriteScene();
@@ -134,3 +142,116 @@ describe('render layers (#302)', () => {
});
});
});
/**
* A coin on `Default` and a HUD container with a sprite in it, a main camera that sees `Default` and
* an overlay that sees `UI`. Behaviors change the layers with Properties nodes, the way a graph does
* it (#305): the coin goes to `UI` on `u` and back to Inherit on `i`, the HUD to `UI` on `h`, and the
* main camera sees both layers on `s`. On `l` the HUD's sprite asks `Render layer of` of itself, of
* the HUD and of its behavior, which is not drawn.
*/
function layerSwitchScene() {
const f = sceneWithCamera();
f.stack.execute(new SetLayerNamesCommand('renderLayers', ['Default', 'UI']));
const coin = place(f, f.spriteId, { x: 0, y: 0 });
const hud = place(f, f.containerId, { x: 0, y: 0 });
const inside = place(f, f.spriteId, { x: 0, y: 0 }, hud);
const main = place(f, f.cameraId, { x: 0, y: 0 });
const over = place(f, f.cameraId, { x: 0, y: 0 });
f.set(main, SEES_KEY, 0b01);
f.set(over, SEES_KEY, 0b10);
f.set(over, OVERLAY_KEY, true);
const behaviorOn = (objectId: string, name: string) => {
const behavior = makeBehavior(f, name);
f.stack.execute(new PlaceAssetCommand(f.sceneId, behavior.id, { x: 0, y: 0 }, objectId));
return graphEditor(f, behavior);
};
const setOnKey = (edit: GraphEditor, key: string, type: string, port: string, value: number) => {
const set = edit.properties([{ key: port, kind: 'number', write: true }], {
[OBJECT_TYPE_PARAM]: type,
[port]: value,
});
edit.connect(edit.addNode('Owner'), 'object', set, 'object');
edit.connect(edit.addNode('OnKeyDown', { key }), 'exec', set, 'exec');
};
const onCoin = behaviorOn(coin, 'Coin');
setOnKey(onCoin, 'u', 'sprite', RENDER_LAYER_KEY, 1);
setOnKey(onCoin, 'i', 'sprite', RENDER_LAYER_KEY, INHERIT_LAYER);
const onHud = behaviorOn(hud, 'Hud');
setOnKey(onHud, 'h', 'container', RENDER_LAYER_KEY, 1);
const onCamera = behaviorOn(main, 'Cam');
setOnKey(onCamera, 's', 'camera', SEES_KEY, 0b11);
const onInside = behaviorOn(inside, 'Inside');
let from = onInside.addNode('OnKeyDown', { key: 'l' });
const ask = (source: GraphNode, port: string) => {
const of = onInside.addNode('RenderLayerOf');
onInside.connect(source, port, of, 'object');
for (const output of ['name', 'number']) {
const log = onInside.addNode('Log');
onInside.connect(of, output, log, 'message');
onInside.connect(from, 'exec', log, 'exec');
from = log;
}
};
const owner = onInside.addNode('Owner');
const parent = onInside.addNode('Parent');
onInside.connect(owner, 'object', parent, 'object');
ask(owner, 'object');
ask(parent, 'parent');
ask(onInside.addNode('Self'), 'object');
return { ...f, coin, hud, inside };
}
describe('a graph changes render layers and what a camera sees during the run (#305)', () => {
const start = () => {
const s = layerSwitchScene();
const started = startWorld(s, { behaviors: true });
const press = (key: string) => {
started.input.press(key);
started.frame();
started.input.release(key);
};
const seenBy = (camera: { props: PropertyBag } | null) => {
const items = collectWorldItems(started.world).items.filter((item) => item.type === 'sprite');
const seen = items.filter((item) => camera && cameraSees(camera.props, item.renderLayer));
return seen.map((item) => (item.id === s.coin ? 'coin' : item.id === s.inside ? 'inside' : item.id));
};
const main = () => seenBy(cameraOfWorld(started.world));
const overlay = () => seenBy(overlaysOfWorld(started.world)[0]!);
started.frame();
return { ...s, ...started, press, main, overlay };
};
it('moves the coin into the overlay with Set, and back with Inherit', () => {
const r = start();
expect([r.main(), r.overlay()]).toEqual([['coin', 'inside'], []]);
r.press('u');
expect([r.main(), r.overlay()]).toEqual([['inside'], ['coin']]);
r.press('i');
expect([r.main(), r.overlay()]).toEqual([['coin', 'inside'], []]);
});
it('takes the HUD container’s sprite along when the container changes', () => {
const r = start();
r.press('h');
expect([r.main(), r.overlay()]).toEqual([['coin'], ['inside']]);
});
it('lets the main camera see a second layer once its Sees is set', () => {
const r = start();
r.press('h');
r.press('s');
expect(r.main()).toEqual(['coin', 'inside']);
});
it('names the layer that holds, the inherited one too, and none for what is not drawn', () => {
const r = start();
r.press('l');
expect(r.world.log).toEqual(['Default', '0', 'Default', '0', '', '-1']);
r.press('h');
r.press('l');
expect(r.world.log.slice(6)).toEqual(['UI', '1', 'UI', '1', '', '-1']);
});
});