Throttle what the editor shows while a game runs (#265)
With the behaviors fixed, the scene Performance test still ran at about 23 fps with 500 entities. The rest of the frame went to the editor: every spawn or destroy rebuilt the hierarchy with all its rows, redrew the scene view behind the game tab and refreshed every panel. The game itself is cheap. What the panels show of it now follows a preset in the project settings. - New section While playing in the project settings with three presets: Live, Balanced and Minimal. Every project reads as Balanced, including those saved before the setting existed. - Live keeps today's behavior. Balanced brings hierarchy and panel values up to date 4 times a second and stops redrawing the hidden scene view. Minimal updates hierarchy and values only on pause and throttles the graph tabs as well. - The graph and state machine tabs stay live under Balanced. At 4 updates a second the beads on exec edges stutter, and the tabs draw only while on screen. This differs from the issue proposal. - The game itself is never throttled. Pause, step and stop bring every panel up to date at once, and a change of structure that falls between two updates is kept for the next one. - The rule lives in editor/playThrottle.ts as panelPolicy and PanelClock. PlaySession asks it after every frame. - Objects spawned at the top level go into a hierarchy group named after their asset, so hundreds of gold pieces fold into one row. Groups start folded. Measured on a throwaway copy of Performance test, 3 s each, fps: - About 500 entities: Live 38.6, Balanced 56, Minimal 60.6. - About 315 entities: Live 56.9, Balanced 60.5, Minimal 60.6. - p95 frame time at 500 entities: 60 ms, 41 ms and 23 ms. Tests: - ./check.sh: 193 files, 2799 tests, CHECK EXIT 0. - GM_STEPS=1,18,59: ALL PASSED, 54 checks. - Full e2e run (local): ALL PASSED, 1562 checks. - tests/editor/play-throttle.test.ts: presets, update rates, kept structure changes, flush, spawn group and loading old projects. - New e2e module 59: preset chosen with the mouse, measured update rates, scene view behind the game tab, Minimal with pause. - Module 03 now expects spawned objects in their group row after the next update. - Counter-checks: 4 in unit tests and 2 in the browser, each failing as expected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a7e793f6de
commit
94c1055895
20 files changed
+592
-19
No files matched your search
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* While playing (#265): how closely the panels follow a running game. The preset is picked in the
|
||||
* Project settings with the mouse; Live brings the panels up to date every frame, Balanced four
|
||||
* times a second, and neither holds the game back. On Balanced the scene view stops drawing behind
|
||||
* the game tab and catches up when it comes back; on Minimal the hierarchy waits for a pause. A
|
||||
* crate spawned at the top level is listed in a group named "Crate".
|
||||
*/
|
||||
export async function whilePlaying(t) {
|
||||
const {
|
||||
check,
|
||||
json,
|
||||
evaluate,
|
||||
click,
|
||||
clickText,
|
||||
clickToolbar,
|
||||
press,
|
||||
play,
|
||||
stop,
|
||||
shot,
|
||||
showTab,
|
||||
undo,
|
||||
sleep,
|
||||
} = t;
|
||||
const playInfo = () => json('__gm.playInfo()');
|
||||
/** Panel updates so far and the page's clock, so a rate is measured in the page, not over CDP. */
|
||||
const sample = () => json('({ at: performance.now(), ...__gm.playInfo().panelUpdates })');
|
||||
const valuesPerSecond = async (ms) => {
|
||||
const a = await sample();
|
||||
await sleep(ms);
|
||||
const b = await sample();
|
||||
return { rate: ((b.values - a.values) * 1000) / (b.at - a.at), a, b };
|
||||
};
|
||||
const setWhilePlaying = async (label) => {
|
||||
await clickToolbar('menu');
|
||||
await clickText('Project settings…');
|
||||
const before = (await json('__gm.projectSettings()')).whilePlaying;
|
||||
const at = await json('__gm.projectSettingsWhilePlayingAt()');
|
||||
await click(at.x, at.y);
|
||||
await clickText(label);
|
||||
const chosen = (await json('__gm.projectSettings()')).whilePlaying;
|
||||
await clickText('Apply');
|
||||
return { before, chosen };
|
||||
};
|
||||
const undoSteps = () => json('__gm.state.commands.undoStack.length');
|
||||
/** Whether the hierarchy *shows* the row – `hierarchyOrder` reads the model, not the list on screen. */
|
||||
const listed = (rowId) => json(`__gm.hierarchyRow(${JSON.stringify(rowId)}) !== null`);
|
||||
const crateGroups = () => json(`(__gm.state.activeScene.groups ?? []).filter((g) => g.name === 'Crate')`);
|
||||
|
||||
await press('Escape');
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await showTab('scene');
|
||||
|
||||
// 59a. The fixture was written without the setting, so it reads as Balanced; Live is picked with
|
||||
// the mouse and is one undo step.
|
||||
check(
|
||||
'a project written before there was a choice plays Balanced',
|
||||
(await json('__gm.state.project.whilePlaying')) === 'balanced',
|
||||
);
|
||||
const steps = await undoSteps();
|
||||
const picked = await setWhilePlaying('Live');
|
||||
check(
|
||||
'the Project settings show Balanced and take Live from the menu',
|
||||
picked.before === 'balanced' &&
|
||||
picked.chosen === 'live' &&
|
||||
(await json('__gm.state.project.whilePlaying')) === 'live' &&
|
||||
(await undoSteps()) === steps + 1,
|
||||
JSON.stringify({ picked, steps: await undoSteps(), before: steps }),
|
||||
);
|
||||
|
||||
// 59b. Live follows every frame; Balanced four times a second.
|
||||
await play();
|
||||
const live = await valuesPerSecond(1500);
|
||||
await stop();
|
||||
await undo();
|
||||
check('undo sets it back to Balanced', (await json('__gm.state.project.whilePlaying')) === 'balanced');
|
||||
await play();
|
||||
const balanced = await valuesPerSecond(1500);
|
||||
check(
|
||||
'Live brings the panels up to date every frame, Balanced at most four times a second',
|
||||
live.rate > 20 && balanced.rate > 2 && balanced.rate <= 4.5,
|
||||
JSON.stringify({ live: live.rate, balanced: balanced.rate }),
|
||||
);
|
||||
|
||||
// 59c. Behind the game tab the scene view does not draw on Balanced – three crates spawned, not
|
||||
// one redraw – and catches up when its tab comes back. The crates are in a group "Crate".
|
||||
const hidden = (await json('__gm.viewportInfo()')).redraws;
|
||||
const entitiesBefore = (await json('__gm.world()')).entities.length;
|
||||
for (let i = 0; i < 3; i++) await press('Space');
|
||||
await sleep(600);
|
||||
const stillHidden = (await json('__gm.viewportInfo()')).redraws;
|
||||
const groups = await crateGroups();
|
||||
const members = await json(
|
||||
`__gm.state.activeScene.objects.filter((o) => o.groupId === ${JSON.stringify(groups[0]?.id ?? '')}).length`,
|
||||
);
|
||||
check(
|
||||
'the game ran on – three crates spawned – and they are listed in one group named Crate',
|
||||
(await json('__gm.world()')).entities.length === entitiesBefore + 3 &&
|
||||
groups.length === 1 &&
|
||||
members === 3,
|
||||
JSON.stringify({ entitiesBefore, groups, members }),
|
||||
);
|
||||
check('the hierarchy shows the group after the next update', await listed(`group:${groups[0]?.id}`));
|
||||
check(
|
||||
'behind the game tab the scene view did not draw once',
|
||||
stillHidden === hidden,
|
||||
JSON.stringify({ hidden, stillHidden }),
|
||||
);
|
||||
await showTab('scene');
|
||||
const shown = (await json('__gm.viewportInfo()')).redraws;
|
||||
check(
|
||||
'and it draws again when its tab comes back',
|
||||
shown > stillHidden,
|
||||
JSON.stringify({ stillHidden, shown }),
|
||||
);
|
||||
await shot('59a-balanced-group');
|
||||
await stop();
|
||||
|
||||
// 59d. Minimal: the hierarchy waits while the game runs, and a pause brings it up to date at once.
|
||||
await showTab('scene');
|
||||
await setWhilePlaying('Minimal');
|
||||
await play();
|
||||
await press('Space');
|
||||
await sleep(600);
|
||||
const spawnedGroups = await crateGroups();
|
||||
const groupRow = `group:${spawnedGroups[0]?.id}`;
|
||||
const listedRunning = await listed(groupRow);
|
||||
check(
|
||||
'on Minimal a spawn reaches the scene, but the hierarchy waits',
|
||||
spawnedGroups.length === 1 && !listedRunning,
|
||||
JSON.stringify({ spawnedGroups, listedRunning }),
|
||||
);
|
||||
await clickToolbar('pause');
|
||||
check('and a pause lists it at once', (await playInfo()).paused && (await listed(groupRow)));
|
||||
await shot('59b-minimal-paused');
|
||||
await stop();
|
||||
|
||||
await undo();
|
||||
check(
|
||||
'the fixture is left on Balanced with the undo stack as found',
|
||||
(await json('__gm.state.project.whilePlaying')) === 'balanced' && (await undoSteps()) === steps,
|
||||
JSON.stringify({ steps: await undoSteps(), before: steps }),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user