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>
81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
/**
|
||
* `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']);
|
||
});
|
||
});
|