Server actually ticks
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// runs the game at a fixed rate. a slow tick makes the next ones run back to back until the game catches up,
|
||||
// so game time keeps up with real time. if it falls too far behind, it skips ahead instead and says so
|
||||
import { TICKS_PER_SECOND } from "$/common/constants.ts";
|
||||
|
||||
// at most this many ticks in a row to catch up, then skip the rest
|
||||
const MAX_CATCH_UP_TICKS = 10;
|
||||
// don't repeat the can't keep up warning more often than this
|
||||
const WARN_INTERVAL_MS = 15_000;
|
||||
|
||||
export interface LoopClock {
|
||||
now(): number;
|
||||
// run fn after ms, returns something clear() takes
|
||||
schedule(fn: () => void, ms: number): unknown;
|
||||
clear(handle: unknown): void;
|
||||
}
|
||||
|
||||
const real_clock: LoopClock = {
|
||||
now: () => performance.now(),
|
||||
schedule: (fn, ms) => setTimeout(fn, ms),
|
||||
clear: (handle) => clearTimeout(handle as number),
|
||||
};
|
||||
|
||||
export interface LoopStats {
|
||||
// ticks that ran in the last second
|
||||
tps: number;
|
||||
// average milliseconds a tick took over the last second
|
||||
mspt: number;
|
||||
// ticks skipped because the game couldn't keep up, since it started
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
export class GameLoop {
|
||||
readonly tick_ms: number;
|
||||
#tick: () => void;
|
||||
#clock: LoopClock;
|
||||
#timer: unknown;
|
||||
#running = false;
|
||||
#next_tick = 0;
|
||||
#last_warning = -Infinity;
|
||||
|
||||
// ticks in the last second, as [when it started, how long it took]
|
||||
#recent: [number, number][] = [];
|
||||
#skipped = 0;
|
||||
|
||||
constructor(tick: () => void, tps = TICKS_PER_SECOND, clock: LoopClock = real_clock) {
|
||||
this.#tick = tick;
|
||||
this.tick_ms = 1000 / tps;
|
||||
this.#clock = clock;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#running) return;
|
||||
this.#running = true;
|
||||
this.#next_tick = this.#clock.now();
|
||||
this.#run();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#running = false;
|
||||
this.#clock.clear(this.#timer);
|
||||
}
|
||||
|
||||
get stats(): LoopStats {
|
||||
const now = this.#clock.now();
|
||||
const recent = this.#recent.filter(([at]) => now - at < 1000);
|
||||
const total = recent.reduce((sum, [, took]) => sum + took, 0);
|
||||
return { tps: recent.length, mspt: recent.length ? total / recent.length : 0, skipped: this.#skipped };
|
||||
}
|
||||
|
||||
#run() {
|
||||
if (!this.#running) return;
|
||||
|
||||
let ran = 0;
|
||||
while (this.#clock.now() >= this.#next_tick && ran < MAX_CATCH_UP_TICKS) {
|
||||
const started = this.#clock.now();
|
||||
try {
|
||||
this.#tick();
|
||||
} catch (e) {
|
||||
// one bad tick shouldn't stop the world
|
||||
console.error("Tick failed:", e);
|
||||
}
|
||||
const took = this.#clock.now() - started;
|
||||
this.#recent.push([started, took]);
|
||||
this.#next_tick += this.tick_ms;
|
||||
ran++;
|
||||
}
|
||||
|
||||
const now = this.#clock.now();
|
||||
this.#recent = this.#recent.filter(([at]) => now - at < 1000);
|
||||
|
||||
const behind = Math.floor((now - this.#next_tick) / this.tick_ms);
|
||||
if (behind > 0) {
|
||||
// catching up would mean running the game in fast forward for a while, skip instead
|
||||
this.#skipped += behind;
|
||||
this.#next_tick += behind * this.tick_ms;
|
||||
if (now - this.#last_warning > WARN_INTERVAL_MS) {
|
||||
this.#last_warning = now;
|
||||
console.warn(
|
||||
`Can't keep up, skipped ${behind} ticks (${Math.round(behind * this.tick_ms)} ms). ` +
|
||||
`Ticks take ${this.stats.mspt.toFixed(1)} ms on average, ${this.tick_ms} ms is the budget.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#timer = this.#clock.schedule(() => this.#run(), Math.max(0, this.#next_tick - this.#clock.now()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user