Files
feuerwerk/src/editor/playThrottle.ts
T
adminandClaude Opus 5.5 94c1055895
game-maker / gate (push) Successful in 3m40s
game-maker / suite (push) Failing after 11m9s
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>
2026-09-29 11:53:04 +02:00

82 lines
3.3 KiB
TypeScript

import type { WhilePlaying } from '../core/model/playSettings';
/**
* How closely the editor follows a running game (#265). The game runs every frame whatever the
* preset says; what is held back is only what the panels show of it. With hundreds of objects
* appearing and vanishing, rebuilding the hierarchy on every one of them cost more than the game.
*/
/** Updates a second on a throttled part. One rate for all of them, so the setting stays one choice. */
export const PANEL_RATE = 4;
/** When a part of the editor catches up: every frame, `PANEL_RATE` times a second, or only on pause. */
export type Follow = 'live' | 'throttled' | 'paused';
export interface PanelPolicy {
/** The hierarchy and every panel that rebuilds when objects appear, vanish or move to a parent. */
structure: Follow;
/** Inspector, variables and tile maps: values that change without an object appearing. */
values: Follow;
/** Pulses and active states in the graph and state machine tabs. */
graphs: 'live' | 'throttled';
/** Whether the scene view keeps redrawing while the game tab hides it. */
hiddenSceneView: boolean;
}
export function panelPolicy(mode: WhilePlaying): PanelPolicy {
switch (mode) {
case 'live':
return { structure: 'live', values: 'live', graphs: 'live', hiddenSceneView: true };
case 'balanced':
// The graph tab draws only while it is on screen and cost nothing measurable; at 4 frames a
// second its beads would stutter and bunch up, so it keeps up with the game.
return { structure: 'throttled', values: 'throttled', graphs: 'live', hiddenSceneView: false };
case 'minimal':
// A graph has no "on pause": a pulse shown only when the game stands still would never be seen.
return { structure: 'paused', values: 'paused', graphs: 'throttled', hiddenSceneView: false };
}
}
/** What to bring up to date after a frame. */
export interface PanelUpdate {
structure: boolean;
values: boolean;
graphs: boolean;
}
/**
* Decides frame by frame what the panels get. A change of structure that falls between two updates
* is kept, not dropped: the next update that is due carries it, and `flush` hands it out at once.
*/
export class PanelClock {
private lastAt = Number.NEGATIVE_INFINITY;
private pendingStructure = false;
/** One frame of the game. `now` is wall time in milliseconds, `structure` whether objects changed. */
frame(policy: PanelPolicy, now: number, structure: boolean): PanelUpdate {
this.pendingStructure ||= structure;
const due = now - this.lastAt >= 1000 / PANEL_RATE;
if (due) this.lastAt = now;
const follows = (follow: Follow) => follow === 'live' || (follow === 'throttled' && due);
const update = {
structure: this.pendingStructure && follows(policy.structure),
values: follows(policy.values),
graphs: policy.graphs === 'live' || due,
};
if (update.structure) this.pendingStructure = false;
return update;
}
/** The game stands still or something must be exact now: everything held back comes out at once. */
flush(): PanelUpdate {
const structure = this.pendingStructure;
this.pendingStructure = false;
return { structure, values: true, graphs: true };
}
/** Whether a change of structure is waiting for its update. */
get holdsStructure(): boolean {
return this.pendingStructure;
}
}