70 lines
1.8 KiB
TypeScript
70 lines
1.8 KiB
TypeScript
import { clamp, settings } from 'shared';
|
|
|
|
const rateGain = 2;
|
|
|
|
const maxRateAdjustment = 0.25;
|
|
|
|
const resyncSeconds = 0.3;
|
|
|
|
/**
|
|
* Playback clock for streamed server state — renders interpolationDelaySeconds
|
|
* behind the server clock so a newer snapshot is usually available; the cursor
|
|
* adjusts its rate gently and only snaps after a long divergence.
|
|
*/
|
|
class ServerTimeline {
|
|
private cursor?: number;
|
|
private newestSnapshotTime?: number;
|
|
private sinceNewestSnapshot = 0;
|
|
private _snapshotTime = 0;
|
|
|
|
public get snapshotTime(): number {
|
|
return this._snapshotTime;
|
|
}
|
|
|
|
public get renderTime(): number {
|
|
return this.cursor ?? 0;
|
|
}
|
|
|
|
public onSnapshot(timestamp: number) {
|
|
this._snapshotTime = timestamp;
|
|
if (this.newestSnapshotTime === undefined || timestamp > this.newestSnapshotTime) {
|
|
this.newestSnapshotTime = timestamp;
|
|
this.sinceNewestSnapshot = 0;
|
|
}
|
|
}
|
|
|
|
// Must be called with unscaled wall-clock time, even during the end-game
|
|
// slow motion: the slowdown is already baked into the snapshots.
|
|
public step(deltaTimeInSeconds: number) {
|
|
if (this.newestSnapshotTime === undefined) {
|
|
return;
|
|
}
|
|
|
|
this.sinceNewestSnapshot += deltaTimeInSeconds;
|
|
const target =
|
|
this.newestSnapshotTime +
|
|
this.sinceNewestSnapshot -
|
|
settings.interpolationDelaySeconds;
|
|
|
|
if (this.cursor === undefined || Math.abs(target - this.cursor) > resyncSeconds) {
|
|
this.cursor = target;
|
|
return;
|
|
}
|
|
|
|
const rate = clamp(
|
|
1 + (target - this.cursor) * rateGain,
|
|
1 - maxRateAdjustment,
|
|
1 + maxRateAdjustment,
|
|
);
|
|
this.cursor += deltaTimeInSeconds * rate;
|
|
}
|
|
|
|
public reset() {
|
|
this.cursor = undefined;
|
|
this.newestSnapshotTime = undefined;
|
|
this.sinceNewestSnapshot = 0;
|
|
this._snapshotTime = 0;
|
|
}
|
|
}
|
|
|
|
export const serverTimeline = new ServerTimeline();
|