Files
feuerwerk/tests/core/fixed-update.test.ts
T
adminandClaude Opus 5.5 ded0022903 Run behaviors on a fixed step with an On fixed update node
Adding up the frame's dt in On update makes anything that counts in
steps depend on the frame rate. A pitch sweep sounds a little
different on every run. On fixed update runs 60 times per second of
game time with dt exactly 1/60, so the same graph gives the same
result every time.

- fixedSteps in src/core/time.ts is the one rule for the fixed step:
  spend the frame in steps of 1/60, keep the rest, catch up 8 steps
  at most after a hitch.
- The physics now counts its steps with fixedSteps too. It behaves
  as before, and On fixed update runs exactly as often as the engine
  steps.
- The steps run after On mouse wheel and before On update, each as a
  kind of its own with the usual Order setting.
- Each step gets its own context, so a node inside that reads dt
  (Time, Move, Apply force) gets 1/60 rather than the frame.
- A paused game has no steps, also for a behavior that runs while
  paused.
- The engine still takes all of a frame's steps before the behaviors
  run. A force set in On fixed update acts from the next frame on.

Tests:
- fixed-update.test.ts (new): 5 tests. Step counting, uneven frames
  adding up to 60 steps a second, the hitch cap, two steps at 30 Hz
  before On update with dt 1/60 for the event and for Time, one step
  every other frame at 120 Hz, none while paused.
- event-order, node-catalog, physics-system, game-pause: 430 tests
  together with the new file, all green.
- Counter-check: with the frame's dt inside a step and without the
  pause guard both world tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:46:15 +02:00

81 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* `On fixed update`: 60 steps a second of game time, each with dt 1/60, however the frames fall –
* counted by the same rule the physics steps by.
*/
import { describe, expect, it } from 'vitest';
import { SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
import { FIXED_STEP, fixedSteps, MAX_FIXED_STEPS } from '../../src/core/time';
import { graphEditor, makeBehavior, makeSpriteScene, place, startWorld } from '../helpers';
describe('fixed steps', () => {
it('spends a frame in steps of 1/60 and keeps the rest for the next', () => {
expect(fixedSteps(0, FIXED_STEP).steps).toBe(1);
expect(fixedSteps(0, 1 / 30).steps).toBe(2);
expect(fixedSteps(0, 0).steps).toBe(0);
const half = fixedSteps(0, 1 / 120);
expect(half.steps).toBe(0);
expect(fixedSteps(half.backlog, 1 / 120).steps).toBe(1);
});
it('adds up to the time that passed, however uneven the frames', () => {
let backlog = 0;
let steps = 0;
for (let i = 0; i < 20; i++)
for (const dt of [0.01, 0.02, 0.005, 0.015]) {
const next = fixedSteps(backlog, dt);
backlog = next.backlog;
steps += next.steps;
}
expect(steps).toBe(60);
});
it('catches up a hitch only so far and forgets the rest', () => {
expect(fixedSteps(0, 1)).toEqual({ steps: MAX_FIXED_STEPS, backlog: 0 });
});
});
/** A behavior logging `fixed <dt of the event> <dt a node reads>` per step and `update` per frame. */
function stepped(runWhilePaused = false) {
const f = makeSpriteScene('Box');
const behavior = makeBehavior(f, 'Engine');
if (runWhilePaused) f.stack.execute(new SetAssetPropertyCommand(behavior.id, 'runWhilePaused', true));
const edit = graphEditor(f, behavior);
const fixed = edit.addNode('OnFixedUpdate');
const eventDt = edit.addNode('Log');
const nodeDt = edit.addNode('Log');
const update = edit.addNode('OnUpdate');
edit.wire([
[fixed, 'exec', eventDt, 'exec'],
[fixed, 'dt', eventDt, 'message'],
[eventDt, 'exec', nodeDt, 'exec'],
[edit.addNode('Time'), 'dt', nodeDt, 'message'],
[update, 'exec', edit.addNode('Log', { message: 'update' }), 'exec'],
]);
place(f, behavior.id, { x: 0, y: 0 });
return startWorld(f, { behaviors: true });
}
const STEP = String(FIXED_STEP);
describe('On fixed update', () => {
it('runs once per 1/60 of game time, before On update, and dt inside is the step', () => {
const { world, frame } = stepped();
frame(1 / 30);
expect(world.log).toEqual([STEP, STEP, STEP, STEP, 'update']);
world.log.length = 0;
frame(1 / 120);
expect(world.log).toEqual(['update']);
frame(1 / 120);
expect(world.log).toEqual(['update', STEP, STEP, 'update']);
});
it('stands still while the game is paused, also for a behavior that runs then', () => {
const { world, frame } = stepped(true);
world.paused = true;
frame(1 / 30);
frame(1 / 30);
expect(world.log).toEqual(['update', 'update']);
});
});