game-maker / suite (push) Successful in 6m48s
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.
The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.
Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.
564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.
Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
131 lines
5.4 KiB
JavaScript
131 lines
5.4 KiB
JavaScript
/**
|
||
* Colliders: "Add collider" from the context menu, collision events reaching the owner's
|
||
* behavior, and Move with "Collide with solids" – blocking, triggers, no tunnelling.
|
||
*/
|
||
export async function colliders(t) {
|
||
const {
|
||
check,
|
||
sleep,
|
||
evaluate,
|
||
json,
|
||
shot,
|
||
click,
|
||
clickText,
|
||
locate,
|
||
press,
|
||
holdKey,
|
||
undo,
|
||
mutate,
|
||
select,
|
||
state,
|
||
object,
|
||
objects,
|
||
asset,
|
||
world,
|
||
entity,
|
||
stop,
|
||
} = t;
|
||
|
||
// 8a. Right-click the red crate → "Add collider": a collider asset sized like the crate
|
||
// (100×60) is placed as its child at the centre, selected and drawn in the overlay. The
|
||
// seed already gives both crates a "Crate collider" child (oc1/oc2), so the added one is
|
||
// undone again to keep exactly one collider per body for the checks below.
|
||
const objectsBefore = (await objects()).length;
|
||
const redRow = await locate('Red crate');
|
||
await click(redRow.x, redRow.y, 'right');
|
||
await clickText('Add collider');
|
||
const s = await state();
|
||
const colObj = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;
|
||
const colAsset = colObj ? await asset(colObj.assetId) : null;
|
||
check(
|
||
'Add collider creates a collider child sized like the sprite',
|
||
colObj?.parentId === 'o1' &&
|
||
colAsset?.type === 'collider' &&
|
||
colAsset.props.width === 100 &&
|
||
colAsset.props.height === 60,
|
||
JSON.stringify({ colObj, colAsset }),
|
||
);
|
||
check(
|
||
'collider is drawn in the overlay layer',
|
||
colObj && (await evaluate(`__gm.layerOf('${colObj.id}')`)) === 'overlay',
|
||
);
|
||
check('collider inspector shows the shape menu', Boolean(await evaluate(`__gm.locate('Box')`)));
|
||
await shot('21-collider');
|
||
await undo();
|
||
const colliderAssetGone = colAsset ? !(await asset(colAsset.id)) : false;
|
||
check(
|
||
'undo removes the added collider object and its asset',
|
||
(await objects()).length === objectsBefore && colliderAssetGone,
|
||
);
|
||
|
||
// 8b. Wire "On collision enter → Log(other)" into the Mover (owner = red crate) and play with
|
||
// the crates overlapping: the log names the blue crate exactly once (enter, not stay),
|
||
// and colliders are not rendered in play mode.
|
||
await mutate(`
|
||
const mover = __gm.state.project.assets.find(a => a.id === 'mover');
|
||
mover.graph.nodes.push({ id: 'n-hit', type: 'OnCollisionEnter', x: 0, y: 600, params: {} }, { id: 'n-hitlog', type: 'Log', x: 340, y: 600, params: {} });
|
||
mover.graph.edges.push(
|
||
{ id: 'e-hit1', from: { node: 'n-hit', port: 'exec' }, to: { node: 'n-hitlog', port: 'exec' } },
|
||
{ id: 'e-hit2', from: { node: 'n-hit', port: 'other' }, to: { node: 'n-hitlog', port: 'message' } });
|
||
__gm.state.activeScene.objects.find(o => o.id === 'o2').position = { x: -100, y: 0, z: 0 }; // 50 from the red crate → 100-wide boxes overlap
|
||
__gm.state.select(null)`);
|
||
await press('Enter', { ctrl: true });
|
||
await sleep(500);
|
||
const collisionWorld = await world();
|
||
check(
|
||
'On collision enter fired once with the other body',
|
||
collisionWorld.log.filter((l) => l === 'Blue crate').length === 1,
|
||
JSON.stringify(collisionWorld.log),
|
||
);
|
||
const spriteCount = await evaluate(
|
||
`__gm.sceneObjects().filter(o => __gm.state.project.assets.find(a => a.id === o.assetId)?.type === 'sprite').length`,
|
||
);
|
||
const info = await json('__gm.gameInfo()');
|
||
check(
|
||
'colliders are not rendered in the game view',
|
||
info.visibleItems === spriteCount,
|
||
`${JSON.stringify(info)} sprites=${spriteCount}`,
|
||
);
|
||
await stop();
|
||
|
||
// 8c. Solid vs trigger. Put the blue crate 150 to the right of the red one (red 100 wide, the
|
||
// blue collider 140 wide → 30 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 = −120.
|
||
// Making the blue collider a trigger lets it pass through instead (drawn amber). A frame
|
||
// step far larger than the collider must not jump across it either (sub-steps).
|
||
const playAgainstBlue = async (trigger, speed = 0) => {
|
||
await mutate(`
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects.find(o => o.id === 'o2').position = { x: 0, y: 0, z: 0 };
|
||
scene.objects.find(o => o.id === 'oc2').overrides = ${trigger ? '{ width: 140, isTrigger: true }' : '{ width: 140 }'};
|
||
${speed ? `__gm.state.project.assets.find(a => a.id === 'mover').props.speed = ${speed};` : ''}
|
||
__gm.state.select(null)`);
|
||
await press('Enter', { ctrl: true });
|
||
await holdKey('ArrowRight', 700);
|
||
const red = await entity('o1');
|
||
await stop();
|
||
return red;
|
||
};
|
||
const blocked = await playAgainstBlue(false);
|
||
check(
|
||
'Move with "Collide with solids" stops the red crate flush against the blue crate\'s solid collider',
|
||
blocked && Math.abs(blocked.x - -120) < 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 > -120 + 20,
|
||
`world x=${passed?.x}`,
|
||
);
|
||
const fast = await playAgainstBlue(false, 40000);
|
||
check(
|
||
'a very fast mover cannot tunnel through a solid collider',
|
||
fast && Math.abs(fast.x - -120) < 0.5,
|
||
`world x=${fast?.x}`,
|
||
);
|
||
await mutate(`__gm.state.project.assets.find(a => a.id === 'mover').props.speed = 200`); // later steps expect the seeded speed
|
||
await select({ kind: 'object', objectId: 'oc2' });
|
||
await shot('22-trigger-collider');
|
||
}
|