Move with collision reacts along the contact normal: it travels to the surface, stays flush and slides the rest of the move along it with Coulomb friction (the mean of both colliders, a tile counts as 0.5), a circle now rolls over an edge instead of catching on it, pushed dynamic bodies move off along the normal, Move gains the outputs hit, normal x and normal y, an object without its own collider moves freely, and editor run and play share the same path through Solids; contact point and normal come from our own f64 geometry (contactBetween), because Rapier's castShape lands 3e-6 px inside the wall in f32 (#150)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d931e66f92
commit
1b5e386b60
15 files changed
+680
-161
No files matched your search
@@ -33,6 +33,34 @@ const walkGraph = {
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Rolls its owner right at 120 per second with "Collide with solids" on, and logs Move's normal
|
||||
* (x, then y) on every frame that hits something.
|
||||
*/
|
||||
const BALL_R = 20;
|
||||
const CORNER_DROP = 12;
|
||||
const rollGraph = {
|
||||
nodes: [
|
||||
node('pr-update', 'OnUpdate', 0, 0),
|
||||
node('pr-owner', 'Owner', 0, 160),
|
||||
node('pr-move', 'Move', 300, 0, { dx: 1, scale: WALK, collide: true }),
|
||||
node('pr-hit', 'Branch', 600, 0),
|
||||
node('pr-log-x', 'Log', 850, 0),
|
||||
node('pr-log-y', 'Log', 1100, 0),
|
||||
],
|
||||
edges: [
|
||||
edge('pr-e1', 'pr-update', 'exec', 'pr-move', 'exec'),
|
||||
edge('pr-e2', 'pr-update', 'dt', 'pr-move', 'dt'),
|
||||
edge('pr-e3', 'pr-owner', 'object', 'pr-move', 'object'),
|
||||
edge('pr-e4', 'pr-move', 'exec', 'pr-hit', 'exec'),
|
||||
edge('pr-e5', 'pr-move', 'hit', 'pr-hit', 'condition'),
|
||||
edge('pr-e6', 'pr-hit', 'then', 'pr-log-x', 'exec'),
|
||||
edge('pr-e7', 'pr-move', 'normalX', 'pr-log-x', 'message'),
|
||||
edge('pr-e8', 'pr-log-x', 'exec', 'pr-log-y', 'exec'),
|
||||
edge('pr-e9', 'pr-move', 'normalY', 'pr-log-y', 'message'),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* A point of a panel (`panel` and `point` are hook expressions), scrolled into view first: a
|
||||
* collider's material rows sit below the fold of the inspector, and after the earlier steps the
|
||||
@@ -305,10 +333,67 @@ export async function physics(t) {
|
||||
const hints = consoleLog.filter((line) => line.includes('[physics]')).slice(hintsBefore);
|
||||
check('nothing in the step made the physics hint at a no-op', hints.length === 0, hints.join(' | '));
|
||||
|
||||
await mutate(`
|
||||
const clearPhysics = () =>
|
||||
mutate(`
|
||||
const p = __gm.state.project;
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('phys-'));
|
||||
p.assets = p.assets.filter((a) => !a.id.startsWith('phys-'));
|
||||
__gm.state.select(null)`);
|
||||
await clearPhysics();
|
||||
|
||||
// 41d. Move along the contact normal (#150): a ball walked straight right into the upper-left
|
||||
// corner of a block, 12 below its centre, rolls up over it instead of hanging on the corner,
|
||||
// and its graph logs the normal of the first contact from Move's hit / normal outputs.
|
||||
const edgeLog = consoleLog.length;
|
||||
await mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
const assets = __gm.state.project.assets;
|
||||
${MAKE_OBJECT}
|
||||
const asset = structuredClone(assets.find((a) => a.id === 'mover'));
|
||||
Object.assign(asset, { id: 'phys-roll', name: 'Physics roll', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(rollGraph)} });
|
||||
assets.push(asset);
|
||||
assets.push({ id: 'phys-col-ball', type: 'collider', name: 'phys-col-ball', props: { shape: 'circle', width: 40, height: 40, radius: ${BALL_R}, isTrigger: false } });
|
||||
assets.push({ id: 'phys-col-block', type: 'collider', name: 'phys-col-block', props: { shape: 'box', width: 100, height: 60, radius: 20, isTrigger: false } });
|
||||
const put = (id, assetId, parentId, x, y, name, overrides = {}) => {
|
||||
const o = make(id, assetId, parentId, y, name);
|
||||
o.position.x = x;
|
||||
o.overrides = overrides;
|
||||
scene.objects.push(o);
|
||||
};
|
||||
put('phys-ball', 'crate', '', 0, ${Y}, 'Rolling ball', { width: 40, height: 40 });
|
||||
put('phys-ball-col', 'phys-col-ball', 'phys-ball', 0, 0);
|
||||
put('phys-ball-roll', 'phys-roll', 'phys-ball', 0, 0, 'Roll');
|
||||
put('phys-block', 'crate', '', 150, ${Y + CORNER_DROP + 30}, 'Edge block', { width: 100, height: 60 });
|
||||
put('phys-block-col', 'phys-col-block', 'phys-block', 0, 0);
|
||||
__gm.state.select(null)`);
|
||||
await play();
|
||||
await sleep(2000);
|
||||
const ball = await json(
|
||||
`(() => { const e = __gm.world()?.entities.find((x) => x.objectId === 'phys-ball'); return e && { x: e.worldX, y: e.worldY }; })()`,
|
||||
);
|
||||
await stop();
|
||||
const top = Y + CORNER_DROP - BALL_R; // the ball's centre when it rolls along the block's top face
|
||||
check(
|
||||
'Move rolls a ball up over the edge it runs into, onto the top face',
|
||||
ball && ball.x > 150 && ball.y <= top + 0.01 && ball.y > top - 3,
|
||||
JSON.stringify({ ball, top }),
|
||||
);
|
||||
const lines = consoleLog.slice(edgeLog).filter((line) => line.startsWith('[behavior '));
|
||||
const logged = lines
|
||||
.filter((line) => line.startsWith('[behavior Roll]'))
|
||||
.map((line) => Number(line.split(']').pop()));
|
||||
const reach = Math.sqrt(BALL_R ** 2 - CORNER_DROP ** 2);
|
||||
check(
|
||||
"Move's hit and normal outputs: the first contact faces from the corner to the ball's centre",
|
||||
logged.length >= 2 &&
|
||||
Math.abs(logged[0] + reach / BALL_R) < 1e-6 &&
|
||||
Math.abs(logged[1] + CORNER_DROP / BALL_R) < 1e-6,
|
||||
JSON.stringify({
|
||||
logged: logged.slice(0, 4),
|
||||
expected: [-reach / BALL_R, -CORNER_DROP / BALL_R],
|
||||
lines: lines.slice(0, 4),
|
||||
}),
|
||||
);
|
||||
await clearPhysics();
|
||||
}
|
||||
Reference in new issue
Block a user