Raycast filtering by collision layers (#158): Colliders gained the "Layers" setting of the Raycast node with the one-sided rule - a ray is not a collider and never reads the collidesWith of what it hits - and the note that a running behavior instance keeps the graph it started with; Physics lost the line that a layer filter for rays comes later and names #157 as the only open point; Behaviors describes the node; Development carries the check sum 1153

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adminandClaude Opus 5 committed 2026-09-20 18:15:40 +02:00
1 parent cf23b4ac5d
commit bc2a138c8c
4 files changed
+37 -28

No files matched your search

+1 -1
@@ -116,7 +116,7 @@ shown on the node and editable in the inspector.
behavior that started it unless `detached` says otherwise), behavior that started it unless `detached` says otherwise),
`Load scene` (swaps the running scene once the frame is over — see [Scenes](Scenes)), `Log`, `Stop game`, `Restart game`, `Load scene` (swaps the running scene once the frame is over — see [Scenes](Scenes)), `Log`, `Stop game`, `Restart game`,
`Apply force`, `Apply impulse`, `Set velocity`, `Set angular velocity`, `Set gravity` (see [Physics](Physics)), `Apply force`, `Apply impulse`, `Set velocity`, `Set angular velocity`, `Set gravity` (see [Physics](Physics)),
`Raycast` (what a ray meets first: object, collider, point, normal, distance — see [Colliders](Colliders#raycast)), `Raycast` (what a ray meets first on the layers it looks at: object, collider, point, normal, distance — see [Colliders](Colliders#raycast)),
`Ignore one-way` (fall through one-way colliders and tiles for some seconds — see [Colliders](Colliders#one-way)). A node type is one file exporting a `NodeDef` `Ignore one-way` (fall through one-way colliders and tiles for some seconds — see [Colliders](Colliders#one-way)). A node type is one file exporting a `NodeDef`
(ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and (ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and
inspector fields are generated from it. A port may declare a `default` for its inline value inspector fields are generated from it. A port may declare a `default` for its inline value
+19 -11
@@ -1,12 +1,12 @@
A **collider** is an asset (`shape` box, circle, capsule or polygon – see A **collider** is an asset (`shape` box, circle, capsule or polygon — see
[Shapes](#shapes)) placed like any [Shapes](#shapes)) placed like any
other object — typically as a child of the sprite it belongs to, so its position is the offset other object — typically as a child of the sprite it belongs to, so its position is the offset
from that *body* and it moves with it. Right-click an object in **Scene objects** → *Add from that *body* and it moves with it. Right-click an object in **Scene objects** → *Add
collider* to get one with one click: a box collider sized like the collider* to get one with one click: a box collider sized like the
sprite, parented at its centre. Colliders can be reused across objects, overridden per instance sprite, parented at its centre. Colliders can be reused across objects, overridden per instance
and stacked (hitbox + hurtbox on one sprite; colliders of the same body never collide with each and stacked (hitbox + hurtbox on one sprite; colliders of the same body never collide with each
other). They are editor symbols: green outlines in edit mode, invisible in play mode. Three more rows – other). They are editor symbols: green outlines in edit mode, invisible in play mode. Three more rows —
friction, bounciness, density – are the material a rigidbody feels (see [Physics](Physics)). friction, bounciness, density — are the material a rigidbody feels (see [Physics](Physics)).
A collider takes the transform of its body: a box turns and grows with the sprite it sits on, a A collider takes the transform of its body: a box turns and grows with the sprite it sits on, a
circle grows (see *Transform* in [Scene editing](Scene-editing)). A turned box is tested as the circle grows (see *Transform* in [Scene editing](Scene-editing)). A turned box is tested as the
@@ -15,15 +15,15 @@ its slant.
## Shapes ## Shapes
- **Box** – `width` × `height`. - **Box** — `width` × `height`.
- **Circle** – `radius`. - **Circle** — `radius`.
- **Capsule** (#151) – fills `width` × `height` and is rounded along the longer side: its radius is - **Capsule** (#151) — fills `width` × `height` and is rounded along the longer side: its radius is
half the shorter side. As wide as tall it is a circle. The shape for a figure that should slide half the shorter side. As wide as tall it is a circle. The shape for a figure that should slide
over edges instead of catching on them. over edges instead of catching on them.
- **Polygon** (#151) – `points`, a list of `{x, y}` corners in the collider's own units, so they - **Polygon** (#151) — `points`, a list of `{x, y}` corners in the collider's own units, so they
turn, grow and mirror with the object. A new polygon collider is a triangle. With the collider turn, grow and mirror with the object. A new polygon collider is a triangle. With the collider
selected in the scene view every corner has a handle: **drag** a corner to move it, **press on an selected in the scene view every corner has a handle: **drag** a corner to move it, **press on an
edge** to put a corner there (and drag it on), **double-click** a corner to take it away – three edge** to put a corner there (and drag it on), **double-click** a corner to take it away — three
corners always stay. Each is one undo step. The list can also be edited in the inspector. corners always stay. Each is one undo step. The list can also be edited in the inspector.
A polygon may be concave: it is cut into convex pieces (ear clipping, then the cuts that are not A polygon may be concave: it is cut into convex pieces (ear clipping, then the cuts that are not
needed are joined again), which is what collision works with. One that crosses itself or has no needed are joined again), which is what collision works with. One that crosses itself or has no
@@ -33,7 +33,7 @@ Box against box, circle against circle and box against circle keep their own exa
pair with a capsule or a polygon goes one common way (`core/properties/convex.ts`): each shape is a pair with a capsule or a polygon goes one common way (`core/properties/convex.ts`): each shape is a
set of convex pieces, each piece a point, a segment or a polygon grown by a radius, and two pieces set of convex pieces, each piece a point, a segment or a polygon grown by a radius, and two pieces
are tested along their separating axes, apart by their real distance. A square polygon meets are tested along their separating axes, apart by their real distance. A square polygon meets
everything exactly as the box it stands for – a unit test holds the two against each other. everything exactly as the box it stands for — a unit test holds the two against each other.
Before the behaviors, every frame sorts the colliders that overlap (world transforms applied; Before the behaviors, every frame sorts the colliders that overlap (world transforms applied;
touching edges do not count) into *enter*, *stay* and *exit*. While playing that is the physics touching edges do not count) into *enter*, *stay* and *exit*. While playing that is the physics
@@ -59,7 +59,7 @@ colliders, a tile counts 0.5). The contact's normal decides which way "along" is
runs into an edge rolls up over it, an object pressed onto a slope slides down it unless the pair runs into an edge rolls up over it, an object pressed onto a slope slides down it unless the pair
is rough enough to hold it (friction ≥ tan of the slope), and one pressed against a wall slides is rough enough to hold it (friction ≥ tan of the slope), and one pressed against a wall slides
along it the slower the harder it presses. Up to four contacts are followed per move (a corner along it the slower the harder it presses. Up to four contacts are followed per move (a corner
between floor and wall is two). There is no bounce: `Move` sets a distance, not a speed – a between floor and wall is two). There is no bounce: `Move` sets a distance, not a speed — a
dynamic body bounces (see [Physics](Physics)). The outputs: `blocked x` / `blocked y` tell which of dynamic body bounces (see [Physics](Physics)). The outputs: `blocked x` / `blocked y` tell which of
the object's own axes something stood against (a floor blocks y, a wall x, a slope each axis the the object's own axes something stood against (a floor blocks y, a wall x, a slope each axis the
move went along); move went along);
@@ -135,6 +135,11 @@ check, a laser, a hitscan shot.
- Met are solid colliders of every shape and the cells of a tilemap, outlined ones too. Triggers - Met are solid colliders of every shape and the cells of a tilemap, outlined ones too. Triggers
only with **Hit triggers** on. A one-way collider or tile only from the side it blocks from. only with **Hit triggers** on. A one-way collider or tile only from the side it blocks from.
- **Layers** (#158) is a tick per collision layer of the project, all of them ticked to begin with:
the ray only meets what sits on one of the ticked layers. That is the one place where the layers
are *not* symmetric — a ray is not a collider, so what it may look at is its own business and a
collider's "Collides with" has no say in it. A shot that passes friends is `Layers` without their
layer; a ground check that ignores the water is the same thing one layer over.
- With **Ignore owner** on (the default) the behavior's owner and everything parented below it are - With **Ignore owner** on (the default) the behavior's owner and everything parented below it are
not met. And a shape the ray *starts inside* is never met — so a ray cast from the middle of a not met. And a shape the ray *starts inside* is never met — so a ray cast from the middle of a
figure finds what is around it; one starting on an outline and running in meets it at 0. figure finds what is around it; one starting on an outline and running in meets it at 0.
@@ -144,4 +149,7 @@ check, a laser, a hitscan shot.
everything is right now — not about the last physics step. everything is right now — not about the last physics step.
- In the editor run every ray cast shows in the scene view for a quarter of a second: green to - In the editor run every ray cast shows in the scene view for a quarter of a second: green to
where it met something, grey to its end when it met nothing. where it met something, grey to its end when it met nothing.
- Only the first hit for now; a list of all hits and a filter by collision layer (#152) come later. - Only the first hit for now; a list of all hits (#157) comes later.
- A setting reaches a *running* instance only when that instance starts afresh: the interpreter
holds the graph it began with, so a behavior that already runs in the editor keeps casting with
the layers it started on until it is switched off and on (or Play is pressed).
+1 -1
@@ -13,7 +13,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
npm run build # writes dist/ and regenerates index.html npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output ./build.sh # the same under `set -o pipefail`, for callers that pipe the output
php tools/make-user.php <name> <password> # an account; every tool signs in (see Accounts) php tools/make-user.php <name> <password> # an account; every tool signs in (see Accounts)
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1148 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 1153 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below ./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./e2e.sh status # live progress, or the last run's result
+16 -15
@@ -1,6 +1,6 @@
Play simulates **rigidbodies** on the physics engine [Rapier 2D](https://rapier.rs) (#146): they Play simulates **rigidbodies** on the physics engine [Rapier 2D](https://rapier.rs) (#146): they
fall, slide, bounce, tip over and come to rest. What has no rigidbody behaves exactly as it did fall, slide, bounce, tip over and come to rest. What has no rigidbody behaves exactly as it did
before the engine – see *Without a rigidbody* below. before the engine — see *Without a rigidbody* below.
## The rigidbody ## The rigidbody
@@ -13,13 +13,13 @@ edit mode and not at all in the game. Its rows:
|---|---|---| |---|---|---|
| Body type | Dynamic | **Dynamic** falls and is pushed. **Static** never moves on its own. **Kinematic** goes wherever the object is put (a graph, an animation) and pushes dynamic bodies out of its way. | | Body type | Dynamic | **Dynamic** falls and is pushed. **Static** never moves on its own. **Kinematic** goes wherever the object is put (a graph, an animation) and pushes dynamic bodies out of its way. |
| Mass (0 = auto) | 0 | 0 lets the colliders' density and size decide; anything else is the mass itself, in kg. | | Mass (0 = auto) | 0 | 0 lets the colliders' density and size decide; anything else is the mass itself, in kg. |
| Gravity scale | 1 | How strongly the scene's gravity pulls; 0 floats, −1 falls upwards. | | Gravity scale | 1 | How strongly the scene's gravity pulls; 0 floats, -1 falls upwards. |
| Linear damping | 0 | Slows the movement down by itself, like air. | | Linear damping | 0 | Slows the movement down by itself, like air. |
| Angular damping | 0 | The same for turning. | | Angular damping | 0 | The same for turning. |
| Fixed rotation | off | On: the body never turns (a player). Off: a box that lands on an edge tips over. | | Fixed rotation | off | On: the body never turns (a player). Off: a box that lands on an edge tips over. |
| Continuous (CCD) | off | For small, fast bodies that would otherwise pass through thin walls between two steps. | | Continuous (CCD) | off | For small, fast bodies that would otherwise pass through thin walls between two steps. |
The body carries every collider below its object – the object's own collider children and those of The body carries every collider below its object — the object's own collider children and those of
objects parented further down, unless one of those has a rigidbody of its own. A rigidbody that is objects parented further down, unless one of those has a rigidbody of its own. A rigidbody that is
switched off is as if it were not there. A body without any solid collider still falls, with its switched off is as if it were not there. A body without any solid collider still falls, with its
mass or 1 kg. mass or 1 kg.
@@ -45,7 +45,7 @@ A trigger (`isTrigger`) weighs nothing and never pushes; it only reports.
A capsule collider is the engine's capsule; a polygon is one convex polygon per piece it is cut A capsule collider is the engine's capsule; a polygon is one convex polygon per piece it is cut
into (see [Colliders](Colliders#shapes)), all on the same body, so a concave shape rests and rolls into (see [Colliders](Colliders#shapes)), all on the same body, so a concave shape rests and rolls
as the shape it is – a crate dropped into a U lands on its inner floor, not on the lid across the as the shape it is — a crate dropped into a U lands on its inner floor, not on the lid across the
notch. A rigidbody's fixed **Mass** is shared out among the pieces by area, and density weighs each notch. A rigidbody's fixed **Mass** is shared out among the pieces by area, and density weighs each
by its own area. by its own area.
@@ -61,7 +61,7 @@ It is read every frame, so an animation over the scene can change it while playi
The engine steps at a fixed **60 steps per second**, whatever the frames take; a frame that took The engine steps at a fixed **60 steps per second**, whatever the frames take; a frame that took
longer catches up with at most 8 steps and forgets the rest, so a hitch does not make the game run longer catches up with at most 8 steps and forgets the rest, so a hitch does not make the game run
fast for a while after. What the game view shows of a dynamic body lies between the last two steps, fast for a while after. What the game view shows of a dynamic body lies between the last two steps,
so it moves evenly on any display – which also means it is drawn up to one step (1/60 s) behind the so it moves evenly on any display — which also means it is drawn up to one step (1/60 s) behind the
engine. engine.
## Collision events ## Collision events
@@ -81,17 +81,17 @@ made in the same frame.
An object with colliders but no rigidbody is what every project was before: its colliders go An object with colliders but no rigidbody is what every project was before: its colliders go
wherever the object is put, nothing pulls on them, and dynamic bodies bounce off them as off a wall. wherever the object is put, nothing pulls on them, and dynamic bodies bounce off them as off a wall.
Whether two of them touch is decided by the same overlap test as before – touching edges do not Whether two of them touch is decided by the same overlap test as before — touching edges do not
count, only overlapping does – the engine only proposes which pairs to test. A placed tilemap is count, only overlapping does — the engine only proposes which pairs to test. A placed tilemap is
static ground made of its merged solid cells (see [Tilemaps](Tilemaps)). `Move` with static ground made of its merged solid cells (see [Tilemaps](Tilemaps)). `Move` with
*Collide with solids* works as described in [Colliders](Colliders): the object stops flush at what *Collide with solids* works as described in [Colliders](Colliders): the object stops flush at what
is in its way and goes on along its surface, less what friction takes – the same friction a is in its way and goes on along its surface, less what friction takes — the same friction a
dynamic body feels, combined the same way (the average), and the same in Play and the editor run. dynamic body feels, combined the same way (the average), and the same in Play and the editor run.
A dynamic body that stops such a move gets **pushed** into the contact – along its normal, so a A dynamic body that stops such a move gets **pushed** into the contact — along its normal, so a
crate hit at its corner is shoved away from the corner, not along the move – as one body meeting another crate hit at its corner is shoved away from the corner, not along the move — as one body meeting another
without bounce: the speed it is pushed with is the move spread over the frame, and how much of it without bounce: the speed it is pushed with is the move spread over the frame, and how much of it
the body takes depends on the two masses – a mover as heavy as a crate shoves it along, a crate the body takes depends on the two masses — a mover as heavy as a crate shoves it along, a crate
forty times heavier hardly shifts (measured: 133 of 161 units in a second, and 0.2). The mover forty times heavier hardly shifts (measured: 133 of 161 units in a second, and 0.2). The mover
weighs what its rigidbody's **Mass** says, or else what its colliders weigh by their density; a weighs what its rigidbody's **Mass** says, or else what its colliders weigh by their density; a
dynamic mover what the engine says. The mover stays flush in front of the crate and never gets dynamic mover what the engine says. The mover stays flush in front of the crate and never gets
@@ -152,7 +152,7 @@ first time the console points to the nodes above, which move it with the physics
## Play and the editor run ## Play and the editor run
The editor loads the engine right after opening the project; Play waits for it if it is not there The editor loads the engine right after opening the project; Play waits for it if it is not there
yet (*Loading the physics engine…*), and does not start if it could not be loaded – the notice and yet (*Loading the physics engine…*), and does not start if it could not be loaded — the notice and
the console say why. There is no falling back to the old detection: a game would collide the console say why. There is no falling back to the old detection: a game would collide
differently. differently.
@@ -161,11 +161,12 @@ are placed, because what the editor run moves is saved. The nodes above do nothi
## What is not there yet ## What is not there yet
- `Move` stepping up onto a ledge, or a limit on how steep a slope it walks up – that is for the - `Move` stepping up onto a ledge, or a limit on how steep a slope it walks up — that is for the
graph, with `hit` and `normal` (#150). `Move` does not use Rapier's character controller: in #147 graph, with `hit` and `normal` (#150). `Move` does not use Rapier's character controller: in #147
it caught on the seams between boxes lying side by side (10 of 100 steps along a floor of 12 it caught on the seams between boxes lying side by side (10 of 100 steps along a floor of 12
crates), kept a gap of up to 1.7 units and knows no friction; `Move` works out its contacts itself, crates), kept a gap of up to 1.7 units and knows no friction; `Move` works out its contacts itself,
in double precision, and slides over the same seams without catching (#150). in double precision, and slides over the same seams without catching (#150).
- The simulation in the editor run (#148). - The simulation in the editor run (#148).
- Joints (#154), and a ray filtered by collision layers (#158). Raycasts (#153), one-way platforms - Joints (#154), and "Raycast all" as a list of every hit (#157). Raycasts (#153) with their layer
(#155) and the collision layers themselves (#152) are there, see [Colliders](Colliders#raycast). filter (#158), one-way platforms (#155) and the collision layers themselves (#152) are there,
see [Colliders](Colliders#raycast).