Merge Move and slide into Move with a 'Collide with solids' toggle

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 15:50:52 +02:00
1 parent 6ecb20cf02
commit b0860ca07f
5 files changed
+24 -34

No files matched your search

+6 -5
View File
@@ -103,15 +103,16 @@ and `collider` (the own one). `Is touching(a, b)` answers the same question on d
object stands for its collider children, a collider for itself. The geometry lives in
`src/core/properties/collider.ts` so editor and runtime agree on it.
**Blocking** is opt-in per movement: the `Move and slide` node (same inputs as `Move`) moves
an object one axis at a time and, after each axis, pushes its *solid* colliders back to just
touch any solid collider they ran into – so the other axis still moves and the object slides
along walls. Its `blocked x` / `blocked y` outputs tell which axes hit. A collider with
**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 and, after each axis, pushes
its *solid* colliders back to just touch any solid collider they ran into – so the other axis
still moves and the object slides along walls. Its `blocked x` / `blocked y` outputs tell which
axes hit (always `false` with the setting off). A collider with
**Trigger (no blocking)** enabled (`isTrigger`, drawn amber instead of green) still produces
collision events but never blocks. Pairs that already overlapped before the step are left
alone, so an object that starts inside a wall can leave it and stacked objects never jolt apart.
Colliders of the moving object and of anything parented under it travel with it and cannot
block it. Plain `Move` / `Set position` ignore colliders entirely.
block it. `Move` without the setting and `Set position` ignore colliders entirely.
## Stack
+11 -22
View File
@@ -293,35 +293,24 @@ registerNode({
type: 'Move',
label: 'Move',
category: 'action',
description: 'Moves an object by (dx, dy) × scale × dt. Connect dt to an event\'s dt for per-second movement; leave it at 1 for per-frame.',
inputs: [exec(), obj('object'), num('dx'), num('dy'), { ...num('scale'), default: 1 }, { ...num('dt'), default: 1 }],
outputs: [exec()],
params: [],
run: (_ctx, i) => {
const target = asEntity(i.object);
if (target) {
const factor = asNumber(i.scale) * asNumber(i.dt);
target.x += asNumber(i.dx) * factor;
target.y += asNumber(i.dy) * factor;
}
return { next: 'exec' };
},
});
registerNode({
type: 'MoveAndSlide',
label: 'Move and slide',
category: 'action',
description: 'Like Move, but solid colliders of the object stop at other solid colliders and slide along them. Outputs which axes were blocked.',
description: 'Moves an object by (dx, dy) × scale × dt. Connect dt to an event\'s dt for per-second movement; leave it at 1 for per-frame. With "Collide with solids" on, the object\'s solid colliders stop at other solid colliders and slide along them; blocked x/y tell which axes hit.',
inputs: [exec(), obj('object'), num('dx'), num('dy'), { ...num('scale'), default: 1 }, { ...num('dt'), default: 1 }],
outputs: [exec(), bool('blockedX', 'blocked x'), bool('blockedY', 'blocked y')],
params: [],
run: (ctx, i) => {
params: [{ key: 'collide', label: 'Collide with solids', kind: 'boolean', default: false }],
run: (ctx, i, p) => {
const target = asEntity(i.object);
let blockedX = false;
let blockedY = false;
if (target) {
const factor = asNumber(i.scale) * asNumber(i.dt);
({ blockedX, blockedY } = ctx.moveAndSlide(target, asNumber(i.dx) * factor, asNumber(i.dy) * factor));
const dx = asNumber(i.dx) * factor;
const dy = asNumber(i.dy) * factor;
if (asBoolean(p.collide)) {
({ blockedX, blockedY } = ctx.moveAndSlide(target, dx, dy));
} else {
target.x += dx;
target.y += dy;
}
}
return { next: 'exec', outputs: { blockedX, blockedY } };
},
+3 -3
View File
@@ -206,12 +206,12 @@ describe('move and slide', () => {
expect(crateA.x).toBe(-95);
});
it('the Move and slide node moves the owner and outputs the blocked axes', () => {
it('the Move node with "Collide with solids" moves the owner and outputs the blocked axes', () => {
const { project, stack, sceneId, behavior, a } = setup();
// On update → Move and slide(Owner, dx = 1000, dt) → Log(blocked x)
// On update → Move(Owner, dx = 1000, dt, collide) → Log(blocked x)
const update = new AddNodeCommand(behavior.id, 'OnUpdate', 0, 0);
const owner = new AddNodeCommand(behavior.id, 'Owner', 0, 0);
const move = new AddNodeCommand(behavior.id, 'MoveAndSlide', 0, 0, { dx: 1000 });
const move = new AddNodeCommand(behavior.id, 'Move', 0, 0, { dx: 1000, collide: true });
const log = new AddNodeCommand(behavior.id, 'Log', 0, 0);
for (const c of [update, owner, move, log]) stack.execute(c);
const connect = (from: AddNodeCommand, fp: string, to: AddNodeCommand, tp: string) =>
+2 -2
View File
@@ -582,7 +582,7 @@ try {
await sleep(200);
// 15b. Solid vs trigger. Put the blue crate 150 to the right of the red one (both 100 wide →
// 50 of free travel) and hold ArrowRight: the Mover's "Move and slide" parks the red
// 50 of free travel) and hold ArrowRight: the Mover's Move (Collide with solids) parks the red
// crate flush against the blue one at world x = −100. Making the blue collider a
// trigger lets it pass through instead (the collider is then drawn amber).
const holdRight = async (ms) => {
@@ -607,7 +607,7 @@ try {
return w.entities.find((e) => e.objectId === 'o1');
};
const blocked = await playAgainstBlue(false);
check('Move and slide stops the red crate flush against the blue crate\'s solid collider', blocked && Math.abs(blocked.x - -100) < 0.01, `world x=${blocked?.x}`);
check('Move with "Collide with solids" stops the red crate flush against the blue crate\'s solid collider', blocked && Math.abs(blocked.x - -100) < 0.01, `world x=${blocked?.x}`);
const passed = await playAgainstBlue(true);
check('a trigger collider does not block (the crate passes through)', passed && passed.x > -100 + 20, `world x=${passed?.x}`);
await evaluate(`__gm.state.select({ kind: 'object', objectId: 'oc2' })`);
+2 -2
View File
@@ -14,7 +14,7 @@ if (!projectId) {
* "Mover" behavior in five nodes: every frame, move the owner along the normalized arrow-key
* axis, scaled by the `speed` variable per second (On update's dt feeds the dt input).
* Diagonals are not faster than straight moves because Axis input normalizes the vector, and
* "Move and slide" stops the owner's solid colliders at other solid colliders.
* Move's "Collide with solids" stops the owner's solid colliders at other solid colliders.
*/
const moverGraph = {
nodes: [
@@ -22,7 +22,7 @@ const moverGraph = {
{ id: 'n-owner', type: 'Owner', x: 0, y: 120, params: {} },
{ id: 'n-axis', type: 'AxisInput', x: 0, y: 200, params: {} },
{ id: 'n-speed', type: 'GetVariable', x: 0, y: 400, params: { variable: 'speed' } },
{ id: 'n-move', type: 'MoveAndSlide', x: 340, y: 0, params: {} },
{ id: 'n-move', type: 'Move', x: 340, y: 0, params: { collide: true } },
],
edges: [
{ id: 'e1', from: { node: 'n-update', port: 'exec' }, to: { node: 'n-move', port: 'exec' } },