Show a spawned object's first animation frame right away
An object that appears during a frame and plays an animation in its On start was drawn once in its original state. A composition item showed frame 0 one frame late. An object from the Spawn node even ran its On start only on the next frame, after it had been drawn. - BehaviorSystem: at the end of the behavior pass, objects that came to be in this frame hear On start and On enable. This covers spawned objects and behaviors switched on for the first time. Their On update starts with the next frame. - AnimationSystem.showStarted: right after that, every animation started in this frame shows its frame 0. The clock runs from the next frame, and an end is told no earlier than then. A paused frame skips this, its animations stand still anyway. - What such a late On start spawns in turn starts in the next frame. Otherwise a spawn chain could stretch one frame without end. - Scene start is unchanged. There On start already ran before the first frame. - Wiki Behaviors, Lifecycle: the new timing. Tests: gate green on stein2 (melektaus-atx went offline mid-run), 305 files, 4716 tests (2 new). e2e 68 on melektaus-atx green, 10 checks. Without the late pass the Spawn test reads 7 instead of 0, without showStarted both new tests are red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
33891acfb3
commit
64d0d472e5
5 files changed
+95
-3
No files matched your search
@@ -362,7 +362,7 @@ export interface NodeContext {
|
||||
loadScene(sceneId: string): void;
|
||||
/**
|
||||
* The position is local to `parent` (world when none); null when the asset does not exist or
|
||||
* cannot be placed. A spawned behavior runs its `On start` on the next tick.
|
||||
* cannot be placed. A spawned behavior runs its `On start` at the end of this tick, before the frame is drawn.
|
||||
*/
|
||||
spawn(
|
||||
assetId: string,
|
||||
|
||||
@@ -51,9 +51,23 @@ export class AnimationSystem implements System {
|
||||
|
||||
update(world: World, dt: number): void {
|
||||
this.finished = [];
|
||||
this.play(world, dt, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows frame 0 of what was started since this frame's tick – by the `On start` of an object that has
|
||||
* only just appeared, say – so the frame is drawn with it rather than with the values from before.
|
||||
* The clock still runs from the next tick, and an end this early is told then.
|
||||
*/
|
||||
showStarted(world: World): void {
|
||||
this.play(world, 0, true);
|
||||
}
|
||||
|
||||
private play(world: World, dt: number, onlyNew: boolean): void {
|
||||
const running: { entity: Entity; player: AnimationPlayer }[] = [];
|
||||
for (const entity of world.entities)
|
||||
for (const player of entity.animations ?? []) running.push({ entity, player });
|
||||
for (const player of entity.animations ?? [])
|
||||
if (!onlyNew || !player.bases) running.push({ entity, player });
|
||||
running.sort((a, b) => a.player.order - b.player.order);
|
||||
// What the players before this one wrote this tick – a blend mixes from that value, live.
|
||||
const written = new Set<string>();
|
||||
@@ -102,6 +116,7 @@ export class AnimationSystem implements System {
|
||||
b.write(value);
|
||||
written.add(b.base);
|
||||
}
|
||||
if (onlyNew) continue;
|
||||
if (blend && share >= 1) delete player.blend;
|
||||
if (fade) {
|
||||
if (fadeShare >= 1) drop(entity, player);
|
||||
|
||||
@@ -670,7 +670,10 @@ export class BehaviorSystem implements System {
|
||||
run(pulsesOf(ticking, 'OnFixedUpdate', { dt: FIXED_STEP }), this.contexts(world, FIXED_STEP));
|
||||
}
|
||||
run(pulsesOf(ticking, 'OnUpdate', { dt }));
|
||||
this.reap(world);
|
||||
// What appeared or was switched on this frame starts before it is drawn, and so does what its
|
||||
// `On start` plays. Objects spawned by such an `On start` wait for the next frame.
|
||||
this.syncInstances(world);
|
||||
if (!frozen) this.animations?.showStarted(world);
|
||||
this.flushStateChanges();
|
||||
this.persist();
|
||||
}
|
||||
|
||||
@@ -1515,6 +1515,46 @@ describe('the animation nodes', () => {
|
||||
expect(startersOf(holder.animations![1]!)).toEqual([]);
|
||||
});
|
||||
|
||||
it('shows frame 0 in the frame a spawned behavior plays it from On start, before it is drawn', () => {
|
||||
const f = makeSpriteScene();
|
||||
const box = place(f, f.spriteId, { x: 7, y: 0 });
|
||||
const walk = addAnimation(f, f.spriteId, { fps: 10 });
|
||||
addRow(f, walk, 'x', [
|
||||
[0, 0],
|
||||
[9, 90],
|
||||
]);
|
||||
const logic = makeBehavior(f, 'Logic', []);
|
||||
const edit = graphEditor(f, logic);
|
||||
const start = edit.addNode('OnStart');
|
||||
const owner = edit.addNode('Owner');
|
||||
const play = edit.addNode('PlayAnimation', { animation: walk.id });
|
||||
edit.wire([
|
||||
[start, 'exec', play, 'exec'],
|
||||
[owner, 'object', play, 'object'],
|
||||
]);
|
||||
// A spawner below the box puts Logic below the box too on a key.
|
||||
const spawner = makeBehavior(f, 'Spawner', []);
|
||||
place(f, spawner.id, { x: 0, y: 0 }, box);
|
||||
const spawning = graphEditor(f, spawner);
|
||||
const key = spawning.addNode('OnKeyDown', { key: 'x' });
|
||||
const here = spawning.addNode('Owner');
|
||||
const spawn = spawning.addNode('Spawn', { asset: logic.id });
|
||||
spawning.wire([
|
||||
[key, 'exec', spawn, 'exec'],
|
||||
[here, 'object', spawn, 'parent'],
|
||||
]);
|
||||
const { world, input, frame } = startWorld(f, { animations: true, behaviors: true });
|
||||
const sprite = world.findByObjectId(box)!;
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBe(7);
|
||||
input.press('x');
|
||||
frame(0.1);
|
||||
expect(sprite.animations).toHaveLength(1);
|
||||
expect(sprite.x).toBe(0);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(10);
|
||||
});
|
||||
|
||||
it('passes Blend on to the player it starts (#106)', () => {
|
||||
const f = makeSpriteScene();
|
||||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
|
||||
@@ -205,6 +205,40 @@ describe('a composition played in a scene', () => {
|
||||
expect(armOf?.props.rotation).toBe(50);
|
||||
});
|
||||
|
||||
it('shows frame 0 of what an item’s own On start plays in the frame the item appears', () => {
|
||||
const stage = makeStage();
|
||||
director(stage);
|
||||
const rig = new CreateAssetCommand('container', 'Rig');
|
||||
const arm = new CreateAssetCommand('sprite', 'Arm');
|
||||
stage.stack.execute(rig);
|
||||
stage.stack.execute(arm);
|
||||
const wave = stage.move(arm.asset.id);
|
||||
const logic = makeBehavior(stage, 'Wave');
|
||||
const edit = graphEditor(stage, logic);
|
||||
const start = edit.addNode('OnStart');
|
||||
const owner = edit.addNode('Owner');
|
||||
const playIt = edit.addNode('PlayAnimation', { animation: wave });
|
||||
edit.connect(start, 'exec', playIt, 'exec');
|
||||
edit.connect(owner, 'object', playIt, 'object');
|
||||
(findAssetOf(stage.project, rig.asset.id, 'container') as ContainerAsset).objects.push(
|
||||
createSceneObject('arm', arm.asset.id, { x: 5, y: 0 }),
|
||||
createSceneObject('wave', logic.id, { x: 0, y: 0 }, 'arm'),
|
||||
);
|
||||
const item = stage.item(first(stage), rig.asset.id, 2, 30);
|
||||
const run = play(stage);
|
||||
const armOf = () => run.world.findByObjectId(contentId(contentId(stage.placement, item), 'arm'));
|
||||
let ticks = 0;
|
||||
while (!armOf() && ticks < 10) {
|
||||
run.frame();
|
||||
ticks++;
|
||||
}
|
||||
// Begun mid-run, on the third tick: drawn with frame 0 of its animation, not where it stood.
|
||||
expect(ticks).toBe(3);
|
||||
expect(armOf()!.x).toBe(0);
|
||||
run.frame();
|
||||
expect(armOf()!.x).toBeCloseTo(10);
|
||||
});
|
||||
|
||||
it('drives the video an item wears at its video time', () => {
|
||||
const stage = makeStage();
|
||||
director(stage);
|
||||
|
||||
Reference in new issue
Block a user