Tell On timer how late its frame is, so time minus late is exact (#224)
A timer can only fire on a frame. It fires on the first frame past each due time, so Time.time at that moment is the due time plus up to one frame. On a display that is not exactly 60 Hz this delay creeps up by a millisecond every few seconds and falls back to zero once it reaches a whole frame. The issue saw 82.009 after 82 ticks. The timer itself does not drift, it keeps the remainder when it fires. - On timer gets a number output late. It is what is left of the timer's clock after taking one interval, so time - late is exactly the due time. - When one long frame catches up several ticks, each gets its own late. The first one was due longest ago and is the latest. Tests: - New unit test in behavior-events: late 0.125 after a frame past 1.0 s, 0 on the dot, 1.5 and 0.5 for two ticks caught up in one frame. - Counter-check: late taken before the interval is subtracted fails exactly that test. Original restored with cmp. - ./check.sh: 156 files, 2300 tests, CHECK EXIT 0 (was 2299). - No e2e change. The e2e modules only wire the exec of On timer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aef1eba917
commit
6fecb2e841
3 files changed
+25
-4
No files matched your search
@@ -1,12 +1,13 @@
|
|||||||
import type { NodeDef } from '../../NodeRegistry';
|
import type { NodeDef } from '../../NodeRegistry';
|
||||||
import { exec } from '../shared';
|
import { exec, num } from '../shared';
|
||||||
|
|
||||||
export const OnTimer: NodeDef = {
|
export const OnTimer: NodeDef = {
|
||||||
type: 'OnTimer',
|
type: 'OnTimer',
|
||||||
label: 'On timer',
|
label: 'On timer',
|
||||||
category: 'event',
|
category: 'event',
|
||||||
description: 'Runs every N seconds.',
|
description:
|
||||||
|
'Runs every N seconds. It can only run on a frame, so late says how far this frame is past the due time.',
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [exec()],
|
outputs: [exec(), num('late')],
|
||||||
params: [{ key: 'interval', label: 'Interval (s)', kind: 'number', default: 1, min: 0.01, step: 0.1 }],
|
params: [{ key: 'interval', label: 'Interval (s)', kind: 'number', default: 1, min: 0.01, step: 0.1 }],
|
||||||
};
|
};
|
||||||
@@ -453,9 +453,11 @@ export class BehaviorSystem implements System {
|
|||||||
i.nodeState.set(node.id, state);
|
i.nodeState.set(node.id, state);
|
||||||
const interval = Math.max(0.01, Number(node.params.interval ?? 1));
|
const interval = Math.max(0.01, Number(node.params.interval ?? 1));
|
||||||
state.elapsed = (state.elapsed ?? 0) + dt;
|
state.elapsed = (state.elapsed ?? 0) + dt;
|
||||||
|
// What is left after taking one interval is how long ago this tick was due. When one
|
||||||
|
// frame catches up several ticks, the first one was due longest ago and is the latest.
|
||||||
while (state.elapsed >= interval) {
|
while (state.elapsed >= interval) {
|
||||||
state.elapsed -= interval;
|
state.elapsed -= interval;
|
||||||
i.fire('OnTimer', ctx, {}, (n) => n.id === node.id);
|
i.fire('OnTimer', ctx, { late: state.elapsed }, (n) => n.id === node.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Neither system ticked in a paused frame: what they hold is last frame's, already told.
|
// Neither system ticked in a paused frame: what they hold is last frame's, already told.
|
||||||
|
|||||||
@@ -262,6 +262,24 @@ describe('On timer', () => {
|
|||||||
expect(world.log).toEqual(['tick', 'tick', 'tick', 'tick']);
|
expect(world.log).toEqual(['tick', 'tick', 'tick', 'tick']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('late is how far the firing frame is past the due time, so time minus late is exact', () => {
|
||||||
|
const f = scene();
|
||||||
|
const edit = graphEditor(f, f.behavior);
|
||||||
|
const timer = edit.addNode('OnTimer', { interval: 1 });
|
||||||
|
const log = edit.addNode('Log', {});
|
||||||
|
edit.connect(timer, 'exec', log, 'exec');
|
||||||
|
edit.connect(timer, 'late', log, 'message');
|
||||||
|
const { world, frame } = startWorld(f, { behaviors: true });
|
||||||
|
|
||||||
|
frame(0.75);
|
||||||
|
frame(0.375); // 1.125 s: due at 1.0
|
||||||
|
expect(world.log).toEqual(['0.125']);
|
||||||
|
frame(0.875); // 2.0 s: due exactly now
|
||||||
|
expect(world.log).toEqual(['0.125', '0']);
|
||||||
|
frame(2.5); // 4.5 s: catches up 3.0 and 4.0, the first one longer ago
|
||||||
|
expect(world.log).toEqual(['0.125', '0', '1.5', '0.5']);
|
||||||
|
});
|
||||||
|
|
||||||
it('two timers keep separate clocks', () => {
|
it('two timers keep separate clocks', () => {
|
||||||
const f = scene();
|
const f = scene();
|
||||||
logOn(f, 'OnTimer', { interval: 1 }, 'slow');
|
logOn(f, 'OnTimer', { interval: 1 }, 'slow');
|
||||||
|
|||||||
Reference in new issue
Block a user