Physics: Move slides along the contact normal with friction, outputs hit/normal in Behaviors, sliding and friction in Colliders and Physics, check total 1057 (#150)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adminandClaude Opus 5 committed 2026-09-18 21:52:11 +02:00
1 parent d6190ec3ee
commit e1e8bbdc98
4 files changed
+28 -14

No files matched your search

+1 -1
@@ -108,7 +108,7 @@ shown on the node and editable in the inspector.
`Length`, `Distance`, `Normalize`, `Axis input` (four keys → a `Length`, `Distance`, `Normalize`, `Axis input` (four keys → a
normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`, normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`,
`Mouse position`, `Mouse button`, `Is hovered`, `Is touching`, `Get velocity`, `Current state` (name and id), `Is in state` (one boolean per state) · `Mouse position`, `Mouse button`, `Is hovered`, `Is touching`, `Get velocity`, `Current state` (name and id), `Is in state` (one boolean per state) ·
actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement; in the object's own axes, which a turned parent turns too), `Set position`, `Spawn` actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement; in the object's own axes, which a turned parent turns too; with *Collide with solids* it slides along what it hits, less friction, and outputs `blocked x/y`, `hit` and the contact `normal x/y`, see [Colliders](Colliders)), `Set position`, `Spawn`
(asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Emit` (starts a particle source over, see *Particles*), (asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Emit` (starts a particle source over, see *Particles*),
`Play sound` / `Stop sound` (see [Sounds](Sounds): the asset says how loud and whether it loops, the node's `Play sound` / `Stop sound` (see [Sounds](Sounds): the asset says how loud and whether it loops, the node's
`volume` multiplies that and its `loop` either leaves it alone or overrules it in either `volume` multiplies that and its `loop` either leaves it alone or overrules it in either
+17 -6
@@ -24,12 +24,23 @@ object stands for its collider children, a collider for itself. The geometry liv
`src/core/properties/collider.ts` so editor and runtime agree on it. `src/core/properties/collider.ts` so editor and runtime agree on it.
**Blocking** is opt-in per movement: the `Move` node has a **Collide with solids** setting **Blocking** is opt-in per movement: the `Move` node has a **Collide with solids** setting
(default off). When enabled it moves an object one axis at a time — its own axes, which a turned (default off). When enabled the object's *solid* colliders move along (dx, dy) — in its own axes,
parent turns as well — and, after each axis, pushes its *solid* colliders back along that axis to which a turned parent turns as well — until one runs into a solid collider; there the object stops
just touch any solid collider they ran into (`exitDistance` in `collider.ts`: the travel at which flush (`exitDistance` in `collider.ts`: the travel at which the shapes stop overlapping). What is left
the shapes stop overlapping) — so the other axis still moves and the object slides along walls. Its `blocked x` / `blocked y` outputs tell which of the move then goes on **along the surface**: the part into it is dropped, and the part along it
axes hit (always `false` with the setting off). In Play a dynamic body that stops the move is pushed is shortened by **friction × the part into it** (Coulomb; the pair's friction is the average of both
along (see [Physics](Physics)). A collider with colliders, a tile counts 0.5). The contact's normal decides which way "along" is, so a circle that
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
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
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
move went along);
`hit` whether anything was met; `normal x` / `normal y` which way the first surface faces, in the
world, pointing away from it — enough to build a bounce or a wall jump in the graph. All are `false`
/ 0 with the setting off. An object without a solid collider of its own moves freely. In Play a
dynamic body that stops the move is pushed into the contact (see [Physics](Physics)). A collider with
**Trigger (no blocking)** enabled (`isTrigger`, drawn amber instead of green) still produces **Trigger (no blocking)** enabled (`isTrigger`, drawn amber instead of green) still produces
collision events but never blocks. An overlap that already exists is never resolved (no jolt collision events but never blocks. An overlap that already exists is never resolved (no jolt
when objects start stacked or inside a wall) but also never deepened, so such an object can when objects start stacked or inside a wall) but also never deepened, so such an object can
+1 -1
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green # code is lost (tail's is reported), so a red run reads as green
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
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1055 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 1057 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
+9 -6
@@ -69,9 +69,11 @@ Whether two of them touch is decided by the same overlap test as before – touc
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 slides along it. 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.
A dynamic body that stops such a move gets **pushed** along the move, as one body meeting another 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
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
@@ -110,9 +112,10 @@ 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` walking up slopes and over steps (#150). Rapier's character controller was measured - `Move` stepping up onto a ledge, or a limit on how steep a slope it walks up – that is for the
for it in #147 and not taken: it caught on the seams between boxes lying side by side (10 of 100 graph, with `hit` and `normal` (#150). `Move` does not use Rapier's character controller: in #147
steps along a floor of 12 crates) and kept a gap of up to 1.7 units, where `Move` catches on it caught on the seams between boxes lying side by side (10 of 100 steps along a floor of 12
nothing and stops flush. 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).
- The simulation in the editor run (#148), and `On collision stay` with contact data (#149). - The simulation in the editor run (#148), and `On collision stay` with contact data (#149).
- Capsules and polygons, collision layers, raycasts, joints, one-way platforms (#144). - Capsules and polygons, collision layers, raycasts, joints, one-way platforms (#144).