The Raycast node filters by collision layers: the node gained a "Layers" setting, a tickbox per collision layer of the project - the same field kind the collider uses for "Collides with" - and the ray only hits what sits on a ticked layer, tilemaps counting as colliders; the default is every layer, and a graph without the param answers exactly as before, which is its own unit test; the check is one-sided on purpose, unlike the symmetric rule of #152: a ray is not a collider, so it never reads the collidesWith of the thing it hits - a collider that meets nothing is still seen by a ray - because who a ray may look at is the ray's business, and a unit test holds that case; the filter sits in Solids.raycast in front of the intersection test, which is the one path the editor run and Play both take, so every unit test runs through describe.each against both systems; NodeView did not know the field kinds layer/layers, so the node would have read "Layers: 65533" - it now shows the name of the layer, or "All layers"; 5 new e2e checks in the new block 43d (a shield on its own layer between the caster and the floor stops the ray, the "Layers" row on the node, the tickbox menu with the project's layers, the ray reaching 280 instead of 145 units once the shield's layer is unticked, and the mask read as a name) and 6 new unit tests, three against each system; two counter-checks hold: disarming the filter line drops 6 unit tests and the unticking check in module 43, and switching the formatting off makes the node check fail with the raw bits; two findings from measuring came out of it and stand in the wiki and the notes - __gm.rays() only measures while the Scene tab is in front, so a node selection blinds it and the block measures through a Log node on distance instead, and a running behavior instance keeps the graph it started with, so the editor run casts with the old mask until the instance is built anew (#158)
game-maker / gate (push) Successful in 2m7s
game-maker / suite (push) Successful in 7m22s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-20 18:15:35 +02:00
1 parent ae3aa7d3bd
commit 0fdde6cb1e
9 files changed
+214 -12

No files matched your search

+3 -2
View File
@@ -158,13 +158,14 @@ export interface NodeContext {
moveAndSlide(target: EntityHandle, dx: number, dy: number): MoveOutcome;
/**
* The first solid a ray from (x, y) along (dx, dy) meets within `length`, all in world units;
* null for none. `ignore` and whatever is parented below it are not met.
* null for none. `ignore` and whatever is parented below it are not met, and neither is anything
* off the layers of `layers` (a 16-bit mask, #158).
*/
raycast(
from: { x: number; y: number },
direction: { x: number; y: number },
length: number,
options: { ignore: EntityHandle | null; triggers: boolean },
options: { ignore: EntityHandle | null; triggers: boolean; layers: number },
): RayOutcome | null;
/** Lets `target` fall through one-way colliders and tiles for `seconds` (#155). */
ignoreOneWay(target: EntityHandle, seconds: number): void;
+11 -3
View File
@@ -1,7 +1,11 @@
import { ALL_LAYERS, layerMaskOf } from '../../../model/collisionLayers';
import type { NodeDef } from '../../NodeRegistry';
import { asBoolean, asNumber } from '../../values';
import { bool, exec, num, obj, withDefault } from '../shared';
/** The mask of layers a ray looks at – shared with `Raycast all` (#157) once that exists. */
export const RAY_LAYERS_PARAM = 'layers';
const MISSED = {
hit: false,
object: null,
@@ -20,9 +24,10 @@ export const Raycast: NodeDef = {
description:
'Casts a ray from (from x, from y) along (dir x, dir y) for up to length world units and says what ' +
'it meets first: the object, its collider (or the tilemap), where, the normal of the face (pointing ' +
'out of it) and how far. Solid colliders and tilemap cells are met, triggers only with "Hit ' +
'triggers", a one-way one only from the side it blocks from. What the ray starts inside is not met, ' +
'and with "Ignore owner" neither is the owner or anything parented below it.',
'out of it) and how far. Solid colliders and tilemap cells are met as long as they sit on one of ' +
'"Layers", triggers only with "Hit triggers", a one-way one only from the side it blocks from. What ' +
'the ray starts inside is not met, and with "Ignore owner" neither is the owner or anything parented ' +
'below it.',
inputs: [
exec(),
num('fromX', 'from x'),
@@ -45,6 +50,8 @@ export const Raycast: NodeDef = {
params: [
{ key: 'ignoreOwner', label: 'Ignore owner', kind: 'boolean', default: true },
{ key: 'triggers', label: 'Hit triggers', kind: 'boolean', default: false },
// Every layer, so a graph written before there was a choice answers as it did (#158).
{ key: RAY_LAYERS_PARAM, label: 'Layers', kind: 'layers', default: ALL_LAYERS },
],
run: (ctx, inputs, params) => {
const hit = ctx.raycast(
@@ -54,6 +61,7 @@ export const Raycast: NodeDef = {
{
ignore: asBoolean(params.ignoreOwner ?? true) ? (ctx.owner ?? ctx.self) : null,
triggers: asBoolean(params.triggers ?? false),
layers: layerMaskOf(params[RAY_LAYERS_PARAM] ?? ALL_LAYERS),
},
);
if (!hit) return { next: 'exec', outputs: MISSED };