76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
/**
|
|
* Fixed-timestep tick loop with an accumulator.
|
|
*
|
|
* The engine advances in whole ticks of `tickMs`. `advance(loop, now)` folds the
|
|
* elapsed wall-clock time into an accumulator and drains it one tick at a time,
|
|
* keeping the sub-tick remainder so results are independent of how the caller
|
|
* chunks calls (determinism — see the test suite).
|
|
*
|
|
* Offline catch-up uses this same path: a large `now` delta owes many ticks.
|
|
* `maxTicks` caps work per `advance()` call; the accumulator retains the
|
|
* remainder so no ticks are lost across calls.
|
|
*
|
|
* The save layer (`save.ts`) separately clamps how much *wall-clock* elapsed
|
|
* time is credited on load via `DEFAULT_MAX_OFFLINE_MS`. Tick loop batching
|
|
* and offline credit clamping are independent concerns.
|
|
*/
|
|
|
|
export const TICK_HZ = 10;
|
|
export const TICK_MS = 1000 / TICK_HZ;
|
|
export const DEFAULT_MAX_TICKS_PER_ADVANCE = 100_000;
|
|
|
|
export interface TickLoop {
|
|
/** Milliseconds of simulated time per tick. */
|
|
readonly tickMs: number;
|
|
/** Total ticks run over this loop's lifetime. */
|
|
tickCount: number;
|
|
/** Sub-tick wall-clock remainder, in milliseconds. */
|
|
accumulatorMs: number;
|
|
/** Last timestamp seen, or null until the first `advance` establishes a baseline. */
|
|
lastNow: number | null;
|
|
}
|
|
|
|
export function createTickLoop(opts?: { tickMs?: number; startNow?: number }): TickLoop {
|
|
const tickMs = opts?.tickMs ?? TICK_MS;
|
|
if (!(tickMs > 0)) {
|
|
throw new RangeError(`tickMs must be > 0, got ${tickMs}`);
|
|
}
|
|
return {
|
|
tickMs,
|
|
tickCount: 0,
|
|
accumulatorMs: 0,
|
|
lastNow: opts?.startNow ?? null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Advance the loop to `now`, running owed ticks (capped at `maxTicks`).
|
|
* Returns the number of ticks run this call.
|
|
*/
|
|
export function advance(
|
|
loop: TickLoop,
|
|
now: number,
|
|
onTick?: (tickIndex: number) => void,
|
|
maxTicks: number = DEFAULT_MAX_TICKS_PER_ADVANCE,
|
|
): number {
|
|
if (loop.lastNow === null) {
|
|
loop.lastNow = now;
|
|
return 0;
|
|
}
|
|
|
|
const elapsed = now - loop.lastNow;
|
|
loop.lastNow = now;
|
|
if (elapsed > 0) {
|
|
loop.accumulatorMs += elapsed;
|
|
}
|
|
|
|
let ran = 0;
|
|
while (loop.accumulatorMs >= loop.tickMs && ran < maxTicks) {
|
|
loop.accumulatorMs -= loop.tickMs;
|
|
loop.tickCount += 1;
|
|
ran += 1;
|
|
onTick?.(loop.tickCount);
|
|
}
|
|
return ran;
|
|
}
|