Start paused, step one frame and break from a behavior
Finding out what a game does in its first frames, or right at one spot of a flow, needed a quick hand on the pause button. Now the editor can start a play paused, step it frame by frame and let a behavior pause it. - The pause button can be pressed in edit mode. Then Play starts paused, and every further Play does too until it is released. A reload of the page clears it. - A third button, Next frame, right of Pause, or Ctrl+Shift+Enter. It works only while paused and runs exactly one frame of 1/60 s, whatever the frame took on screen. Hierarchy and inspector follow. - The Break node pauses the play like the button, at the end of the frame it ran in. The status line names the behavior's object. It is the editor's pause, not Pause game. - Break reaches the editor through a service only the editor hands to a play. In play.html and Run in editor the node does nothing. - Restart game keeps the running play's state. Start paused only counts when Play is pressed. - Wiki Concepts, Behaviors, Keyboard: the buttons, the node and the shortcut. Not tried: sound, free-running videos and a running recording while stepping. Sound and videos stay held during the pause. Tests: - tsc, eslint and Prettier are clean. - Unit pause, node-catalog, tooltip: 465 green. svg-icons now expects the step icon too. - e2e 18 and 46 on stein2 (melektaus-atx was off): ALL PASSED, 63 checks, 7 new in 18j. A step moves the box by 1.667 px at 100 px/s, Break stops the play with "Break in Breaker". - Counter-checks: a session that ignores the step turns 18j red. A Break that does not report its object turns the unit test red. - Gate on stein2: tsc, eslint and Prettier clean, 305 files, 4722 tests, 6 more than before. One was red at first, the icon list of the top bar did not know step yet. Fixed in svg-icons.test.ts and run again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
64d0d472e5
commit
7a952cb20d
23 files changed
+378
-55
No files matched your search
@@ -37,7 +37,7 @@ import type { Entity } from '../Entity';
|
||||
import type { World } from '../World';
|
||||
import type { Rapier } from '../physics/engine';
|
||||
import { type BodyMotion, PhysicsSystem } from '../physics/PhysicsSystem';
|
||||
import { behaviorContext, type TimeScaled } from './behaviorContext';
|
||||
import { behaviorContext, type Breakpoint, type TimeScaled } from './behaviorContext';
|
||||
import {
|
||||
animationFinishedPulses,
|
||||
collisionEnterPulses,
|
||||
@@ -60,7 +60,7 @@ import type { Input } from './Input';
|
||||
import type { RecordingSink } from '../../core/recording/recording';
|
||||
import type { System } from './System';
|
||||
|
||||
export type { TimeScaled } from './behaviorContext';
|
||||
export type { Breakpoint, TimeScaled } from './behaviorContext';
|
||||
|
||||
/** How long (seconds) a traversed edge/node counts as "hot" for the editor's live highlight. */
|
||||
export const TRACE_FADE = 0.35;
|
||||
@@ -124,6 +124,8 @@ export interface GameSystemOptions {
|
||||
playsCompositions?: boolean;
|
||||
/** The view a played game is shown in: a composition waits for its videos there (#417). */
|
||||
pictures?: VideoPictures | null;
|
||||
/** Where a `Break` node pauses: only the editor's Play has one. */
|
||||
breakpoint?: Breakpoint | null;
|
||||
}
|
||||
|
||||
/** Collisions, compositions and animations are ticked first, so behaviors see this frame's events. */
|
||||
@@ -142,12 +144,13 @@ export function addGameSystems(
|
||||
recorder = null,
|
||||
playsCompositions = true,
|
||||
pictures = null,
|
||||
breakpoint = null,
|
||||
}: GameSystemOptions = {},
|
||||
): BehaviorSystem {
|
||||
const collisions = physics ? new PhysicsSystem(physics) : new CollisionSystem();
|
||||
const compositions = new CompositionSystem(playsCompositions, pictures);
|
||||
const animations = new AnimationSystem();
|
||||
const sources = { collisions, animations, compositions, audio, clock, saves, recorder };
|
||||
const sources = { collisions, animations, compositions, audio, clock, saves, recorder, breakpoint };
|
||||
const behaviors = new BehaviorSystem(project, input, sources, shouldRun);
|
||||
if (carried) behaviors.carryOver(carried);
|
||||
if (kept) {
|
||||
@@ -171,6 +174,7 @@ export interface EventSources {
|
||||
clock?: TimeScaled | null;
|
||||
saves?: SaveArea | null;
|
||||
recorder?: RecordingSink | null;
|
||||
breakpoint?: Breakpoint | null;
|
||||
}
|
||||
|
||||
/** A behavior's `runWhilePaused` switch: whether it is ticked while Pause game holds the world. */
|
||||
@@ -216,6 +220,7 @@ export class BehaviorSystem implements System {
|
||||
private readonly clock: TimeScaled | null;
|
||||
private readonly saves: SaveArea | null;
|
||||
private readonly recorder: RecordingSink | null;
|
||||
private readonly breakpoint: Breakpoint | null;
|
||||
/** What was last written to `saves`, so a frame that changed nothing writes nothing. */
|
||||
private lastSaved = '';
|
||||
|
||||
@@ -236,6 +241,7 @@ export class BehaviorSystem implements System {
|
||||
this.clock = sources.clock ?? null;
|
||||
this.saves = sources.saves ?? null;
|
||||
this.recorder = sources.recorder ?? null;
|
||||
this.breakpoint = sources.breakpoint ?? null;
|
||||
}
|
||||
|
||||
/** The values `global` declarations share this run – read-only to callers, rebuilt by `start`. */
|
||||
@@ -775,6 +781,7 @@ export class BehaviorSystem implements System {
|
||||
audio: this.audio,
|
||||
clock: this.clock,
|
||||
recorder: this.recorder,
|
||||
breakpoint: this.breakpoint,
|
||||
shared: this.sharedState,
|
||||
stateChanged: (machineId, from, to) => this.pending.push({ machineId, from, to, source: instance }),
|
||||
});
|
||||
|
||||
@@ -49,6 +49,9 @@ export interface TimeScaled {
|
||||
scale: number;
|
||||
}
|
||||
|
||||
/** Hears a `Break` node, with the name of the object whose behavior ran it. */
|
||||
export type Breakpoint = (where: string) => void;
|
||||
|
||||
/** What the running `BehaviorSystem` lends every context it makes. */
|
||||
export interface ContextSources {
|
||||
project: Project;
|
||||
@@ -58,6 +61,8 @@ export interface ContextSources {
|
||||
clock: TimeScaled | null;
|
||||
/** Where a recording of the game view goes (#405); only a played game has one. */
|
||||
recorder: RecordingSink | null;
|
||||
/** Where `Break` pauses; only the editor's Play has one. */
|
||||
breakpoint: Breakpoint | null;
|
||||
shared: SharedState;
|
||||
/** A graph moved one of this instance's machines; its events follow between exec flows. */
|
||||
stateChanged(machineId: string, from: string, to: string): void;
|
||||
@@ -270,6 +275,7 @@ export function behaviorContext(
|
||||
},
|
||||
startRecording: (settings) => tell(world, run.recorder?.start(settings) ?? NO_RECORDER),
|
||||
stopRecording: () => tell(world, run.recorder?.stop() ?? NO_RECORDER),
|
||||
breakpoint: () => run.breakpoint?.(entity.name),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user