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
@@ -20,6 +20,7 @@ import { World } from '../runtime/World';
|
||||
import { ui } from '../ui';
|
||||
import { soundLibrary } from '../viewport/SoundLibrary';
|
||||
import { saveArea } from './saves';
|
||||
import { PanelClock, panelPolicy, type PanelUpdate } from './playThrottle';
|
||||
import type { GameViewport } from './viewport/GameViewport';
|
||||
import type { GraphEditor } from './graph/GraphEditor';
|
||||
import type { LiveMachineState } from './graph/GraphCanvas';
|
||||
@@ -47,6 +48,8 @@ export interface WorldSnapshot {
|
||||
|
||||
export interface PlayHost {
|
||||
gameView: GameViewport;
|
||||
/** The scene view: while a run holds the panels back, it still draws every frame it is on screen. */
|
||||
viewport: { visible: boolean; redraw(): void };
|
||||
graphEditor: GraphEditor;
|
||||
stateMachineEditor: StateMachineEditor;
|
||||
showGame(): void;
|
||||
@@ -60,11 +63,20 @@ export class PlaySession {
|
||||
private readonly input = new Input();
|
||||
/** Set while Play waits for the physics engine, so a second press does not queue a second run. */
|
||||
private waiting = false;
|
||||
/** When the panels catch up with the run (#265). */
|
||||
private readonly panels = new PanelClock();
|
||||
/** How often the panels were brought up to date this run (test hook). */
|
||||
private updates = { structure: 0, values: 0 };
|
||||
|
||||
constructor(
|
||||
private readonly state: EditorState,
|
||||
private readonly host: PlayHost,
|
||||
) {}
|
||||
) {
|
||||
// An object picked during a run is looked at now: the hierarchy has to list it to show it picked.
|
||||
state.on('selection', () => {
|
||||
if (this.world && this.panels.holdsStructure) this.follow(this.panels.flush());
|
||||
});
|
||||
}
|
||||
|
||||
get isPlaying(): boolean {
|
||||
return this.world !== null;
|
||||
@@ -83,6 +95,8 @@ export class PlaySession {
|
||||
togglePause(): void {
|
||||
if (!this.world) return;
|
||||
this.state.setPaused(!this.state.paused);
|
||||
// What you inspect while paused is exact, whatever the panels were holding back.
|
||||
if (this.state.paused) this.follow(this.panels.flush());
|
||||
// A held note is part of the picture a pause freezes: the sounds stop where they are and carry
|
||||
// on mid-note, rather than playing to their end over a still image.
|
||||
soundLibrary.setSuspended(this.state.paused);
|
||||
@@ -134,6 +148,7 @@ export class PlaySession {
|
||||
const runtime = this.state.enterPlay();
|
||||
const world = World.instantiate(runtime, scene.id);
|
||||
this.world = world;
|
||||
this.updates = { structure: 0, values: 0 };
|
||||
this.input.attach();
|
||||
this.behaviors = addGameSystems(world, runtime, this.input, {
|
||||
audio: soundLibrary,
|
||||
@@ -252,14 +267,43 @@ export class PlaySession {
|
||||
this.loadScene(world.loadSceneRequested);
|
||||
return;
|
||||
}
|
||||
// Mirror what the systems did into the runtime copy so the panels show live values.
|
||||
if (world.syncToModel().structure) this.state.preview();
|
||||
this.state.runtimeTick();
|
||||
// Mirror what the systems did into the runtime copy every frame, even while the panels wait: an
|
||||
// edit made during the run lands on the model, and it must not land on an old picture of it.
|
||||
const structure = world.syncToModel().structure;
|
||||
const update = this.panels.frame(
|
||||
panelPolicy(this.state.project.whilePlaying),
|
||||
performance.now(),
|
||||
structure,
|
||||
);
|
||||
// A game that pauses itself is looked at like one paused from the toolbar.
|
||||
this.follow(world.paused && !wasPaused ? this.panels.flush() : update);
|
||||
}
|
||||
|
||||
/** Brings the panels up to date as far as `update` says; the game's own picture never waits. */
|
||||
private follow(update: PanelUpdate): void {
|
||||
if (update.structure) {
|
||||
this.updates.structure++;
|
||||
this.state.preview();
|
||||
}
|
||||
if (update.values) {
|
||||
this.updates.values++;
|
||||
this.state.runtimeTick();
|
||||
} else {
|
||||
// The views draw on `runtime`, and what is on screen of the game has to move every frame.
|
||||
if (this.host.gameView.visible) this.host.gameView.redraw();
|
||||
if (this.host.viewport.visible) this.host.viewport.redraw();
|
||||
}
|
||||
if (!update.graphs) return;
|
||||
if (this.host.graphEditor.visible) this.host.graphEditor.canvas.redraw();
|
||||
// The machine tab shows the state a run is in, so it repaints for the same reason the graph does.
|
||||
if (this.host.stateMachineEditor.visible) this.host.stateMachineEditor.canvas.redraw();
|
||||
}
|
||||
|
||||
/** How often the panels caught up with this run, by kind (test hook). */
|
||||
panelUpdates(): { structure: number; values: number } {
|
||||
return { ...this.updates };
|
||||
}
|
||||
|
||||
/** The check scripts get the run as JSON, so an object a bag holds is handed over as its name. */
|
||||
snapshot(): WorldSnapshot | null {
|
||||
const world = this.world;
|
||||
|
||||
Reference in new issue
Block a user