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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-05 21:46:15 +02:00
1 parent c7e9769f90
commit ded0022903
8 files changed
+140 -14

No files matched your search

@@ -0,0 +1,17 @@
import type { NodeDef } from '../../NodeRegistry';
import { hiddenOrder, orderParam } from '../../eventOrder';
import { exec, num } from '../shared';
export const OnFixedUpdate: NodeDef = {
type: 'OnFixedUpdate',
label: 'On fixed update',
category: 'event',
description:
'Runs 60 times a second of game time, with dt always 1/60, however long the frame took: a short ' +
'frame may have none, a long one several, each before On update. As often as physics steps. ' +
'Stands still while the game is paused.',
inputs: [],
outputs: [exec(), num('dt')],
params: [orderParam],
hiddenParams: hiddenOrder,
};
+2
View File
@@ -124,6 +124,7 @@ import { OnCollisionStay } from './events/OnCollisionStay';
import { OnDestroy } from './events/OnDestroy';
import { OnDisable } from './events/OnDisable';
import { OnEnable } from './events/OnEnable';
import { OnFixedUpdate } from './events/OnFixedUpdate';
import { OnKeyDown } from './events/OnKeyDown';
import { OnKeyHeld } from './events/OnKeyHeld';
import { OnKeyUp } from './events/OnKeyUp';
@@ -159,6 +160,7 @@ import { Variables } from './variable/Variables';
export const BUILT_IN_NODES: readonly NodeDef[] = [
OnStart,
OnEnable,
OnFixedUpdate,
OnUpdate,
OnDisable,
OnDestroy,