Physics stage 1b: six new nodes act on dynamic bodies in play (Apply force over the frame, independent of the frame rate, Apply impulse, Set velocity, Set angular velocity, Get velocity, Set gravity for the rest of the run), a node on an object without a dynamic body and moving a dynamic body directly each log a hint once per object, and Move with collision now pushes dynamic bodies with a mass-dependent push while the mover stays flush; Move keeps its own move-and-slide instead of Rapier's character controller, which measurably snags on seams between colliders (#147)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78899b1ace
commit
0ff3c8b742
10 files changed
+783
-43
No files matched your search
@@ -1,5 +1,38 @@
|
||||
import { edge, node } from '../lib/builders.mjs';
|
||||
import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||
|
||||
/** Kicks its owner in On start, with the scene's gravity switched off first: it glides at 100 per second. */
|
||||
const KICK = 200;
|
||||
const KICKED_MASS = 2;
|
||||
const kickGraph = {
|
||||
nodes: [
|
||||
node('pk-start', 'OnStart', 0, 0),
|
||||
node('pk-gravity', 'SetGravity', 300, 0, { x: 0, y: 0 }),
|
||||
node('pk-owner', 'Owner', 300, 160),
|
||||
node('pk-kick', 'ApplyImpulse', 600, 0, { x: KICK, y: 0 }),
|
||||
],
|
||||
edges: [
|
||||
edge('pk-e1', 'pk-start', 'exec', 'pk-gravity', 'exec'),
|
||||
edge('pk-e2', 'pk-gravity', 'exec', 'pk-kick', 'exec'),
|
||||
edge('pk-e3', 'pk-owner', 'object', 'pk-kick', 'object'),
|
||||
],
|
||||
};
|
||||
|
||||
/** Walks its owner right at 120 per second with "Collide with solids" on. */
|
||||
const WALK = 120;
|
||||
const walkGraph = {
|
||||
nodes: [
|
||||
node('pw-update', 'OnUpdate', 0, 0),
|
||||
node('pw-owner', 'Owner', 0, 160),
|
||||
node('pw-move', 'Move', 300, 0, { dx: 1, scale: WALK, collide: true }),
|
||||
],
|
||||
edges: [
|
||||
edge('pw-e1', 'pw-update', 'exec', 'pw-move', 'exec'),
|
||||
edge('pw-e2', 'pw-update', 'dt', 'pw-move', 'dt'),
|
||||
edge('pw-e3', 'pw-owner', 'object', 'pw-move', 'object'),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -187,4 +220,97 @@ export async function physics(t) {
|
||||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('phys-') && !gone.has(o.id));
|
||||
p.assets = p.assets.filter((a) => !a.id.startsWith('phys-col-') && !assets.has(a.id));
|
||||
__gm.state.select(null)`);
|
||||
|
||||
// 41c. The graph drives the physics (#147): a crate kicked in On start with gravity switched off
|
||||
// by Set gravity, and a walker whose Move with "Collide with solids" runs into a free crate.
|
||||
const hintsBefore = consoleLog.filter((line) => line.includes('[physics]')).length;
|
||||
await mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
const p = __gm.state.project;
|
||||
const assets = p.assets;
|
||||
${MAKE_OBJECT}
|
||||
const behavior = (id, name, graph) => {
|
||||
const asset = structuredClone(assets.find((a) => a.id === 'mover'));
|
||||
Object.assign(asset, { id, name, props: { enabled: true, runInEditor: false }, variables: [], graph });
|
||||
assets.push(asset);
|
||||
};
|
||||
behavior('phys-kick', 'Physics kick', ${JSON.stringify(kickGraph)});
|
||||
behavior('phys-walk', 'Physics walk', ${JSON.stringify(walkGraph)});
|
||||
assets.push({ id: 'phys-col-box', type: 'collider', name: 'phys-col-box', props: { shape: 'box', width: 40, height: 40, radius: 20, isTrigger: false } });
|
||||
assets.push({ id: 'phys-rb-kicked', type: 'rigidbody', name: 'phys-rb-kicked', props: { bodyType: 'dynamic', mass: ${KICKED_MASS} } });
|
||||
assets.push({ id: 'phys-rb-free', type: 'rigidbody', name: 'phys-rb-free', props: { bodyType: 'dynamic', mass: 0 } });
|
||||
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);
|
||||
};
|
||||
const box = { width: 40, height: 40 };
|
||||
put('phys-kicked', 'crate', '', -200, ${Y}, 'Kicked crate', box);
|
||||
put('phys-kicked-col', 'phys-col-box', 'phys-kicked', 0, 0);
|
||||
put('phys-kicked-rb', 'phys-rb-kicked', 'phys-kicked', 0, 0);
|
||||
put('phys-kicked-kick', 'phys-kick', 'phys-kicked', 0, 0);
|
||||
put('phys-walker', 'crate', '', 0, ${Y + 100}, 'Walker', box);
|
||||
put('phys-walker-col', 'phys-col-box', 'phys-walker', 0, 0);
|
||||
put('phys-walker-walk', 'phys-walk', 'phys-walker', 0, 0);
|
||||
put('phys-pushed', 'crate', '', 60, ${Y + 100}, 'Pushed crate', box);
|
||||
put('phys-pushed-col', 'phys-col-box', 'phys-pushed', 0, 0);
|
||||
put('phys-pushed-rb', 'phys-rb-free', 'phys-pushed', 0, 0);
|
||||
__gm.state.select(null)`);
|
||||
await play();
|
||||
await evaluate(`(() => {
|
||||
const out = (window.__physicsGraph = []);
|
||||
const start = performance.now();
|
||||
const sample = () => {
|
||||
const w = __gm.world();
|
||||
if (!w) return;
|
||||
const at = (id) => w.entities.find((e) => e.objectId === id);
|
||||
const kicked = at('phys-kicked');
|
||||
out.push({
|
||||
t: (performance.now() - start) / 1000,
|
||||
kickedX: kicked?.worldX,
|
||||
kickedY: kicked?.worldY,
|
||||
walker: at('phys-walker')?.worldX,
|
||||
pushed: at('phys-pushed')?.worldX,
|
||||
});
|
||||
if (performance.now() - start < 2000) requestAnimationFrame(sample);
|
||||
};
|
||||
requestAnimationFrame(sample);
|
||||
})()`);
|
||||
await sleep(2300);
|
||||
const run = await json('window.__physicsGraph');
|
||||
await stop();
|
||||
|
||||
const at = (seconds) =>
|
||||
run.reduce((best, s) => (Math.abs(s.t - seconds) < Math.abs(best.t - seconds) ? s : best));
|
||||
const [early, late] = [at(0.5), at(1.5)];
|
||||
const glide = (late.kickedX - early.kickedX) / (late.t - early.t);
|
||||
const speed = KICK / KICKED_MASS;
|
||||
check(
|
||||
`Set gravity and Apply impulse in On start: the crate glides straight at impulse ÷ mass (${speed} per second)`,
|
||||
run.length > 60 &&
|
||||
Math.abs(glide - speed) < speed * 0.05 &&
|
||||
run.every((s) => Math.abs(s.kickedY - Y) < 0.5),
|
||||
JSON.stringify({ frames: run.length, glide: Math.round(glide * 10) / 10, early, late }),
|
||||
);
|
||||
const gap = Math.min(...run.map((s) => s.pushed - 20 - (s.walker + 20)));
|
||||
const pushed = run.at(-1).pushed - 60;
|
||||
check(
|
||||
'Move with "Collide with solids" pushes the crate in its way and never gets into it',
|
||||
pushed > 100 && gap > -0.01,
|
||||
JSON.stringify({
|
||||
pushed: Math.round(pushed * 10) / 10,
|
||||
walked: Math.round(run.at(-1).walker * 10) / 10,
|
||||
gap,
|
||||
}),
|
||||
);
|
||||
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 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)`);
|
||||
}
|
||||
Reference in new issue
Block a user