@hastearcade/snowglobe
Version:
A TypeScript port of CrystalOrb, a high-level Rust game networking library
115 lines • 5.74 kB
JavaScript
import * as Timestamp from './timestamp.js';
export var TerminationCondition;
(function (TerminationCondition) {
TerminationCondition[TerminationCondition["LastUndershoot"] = 0] = "LastUndershoot";
TerminationCondition[TerminationCondition["FirstOvershoot"] = 1] = "FirstOvershoot";
})(TerminationCondition || (TerminationCondition = {}));
export function decomposeFloatTimestamp(condition, floatTimestamp, timestepSeconds) {
let timestamp;
switch (condition) {
case TerminationCondition.LastUndershoot:
timestamp = Timestamp.floor(floatTimestamp);
break;
case TerminationCondition.FirstOvershoot:
timestamp = Timestamp.ceil(floatTimestamp);
break;
}
const overshootSeconds = Timestamp.asSeconds(Timestamp.subFloat(Timestamp.toFloat(timestamp), floatTimestamp), timestepSeconds);
return [timestamp, overshootSeconds];
}
export function shouldTerminate(condition, currentOvershootSeconds, nextOvershootSeconds) {
switch (condition) {
case TerminationCondition.LastUndershoot:
return nextOvershootSeconds > 0;
case TerminationCondition.FirstOvershoot:
return currentOvershootSeconds >= 0;
}
}
export class TimeKeeper {
stepper;
terminationCondition;
timestepOvershootSeconds = 0;
config;
constructor(stepper, config, terminationCondition = TerminationCondition.LastUndershoot) {
this.stepper = stepper;
this.config = config;
this.terminationCondition = terminationCondition;
}
update(deltaSeconds, serverSecondsSinceStartup) {
const startTime = Date.now();
const compensateStart = Date.now();
const compensatedDeltaSeconds = this.deltaSecondsCompensateForDrift(deltaSeconds, serverSecondsSinceStartup);
const compensateEnd = Date.now();
const stepStart = Date.now();
const stepCount = this.advanceStepper(compensatedDeltaSeconds);
const stepEnd = Date.now();
const skipStart = Date.now();
this.timeskipIfNeeded(serverSecondsSinceStartup);
const skipEnd = Date.now();
const postStart = Date.now();
if (stepCount > 0)
this.stepper.postUpdate(this.timestepOvershootSeconds);
const postEnd = Date.now();
if (Date.now() - startTime > 15) {
console.log(`updating timekeeper took too long: ${Date.now() - startTime}`);
console.log(`updating drift took too long: ${compensateEnd - compensateStart}`);
console.log(`updating step took too long: ${stepEnd - stepStart}`);
console.log(`updating timeskip took too long: ${skipEnd - skipStart}`);
console.log(`updating postUpdate took too long: ${postEnd - postStart}`);
}
}
currentLogicalTimestamp() {
return Timestamp.subFloat(Timestamp.toFloat(this.stepper.lastCompletedTimestamp()), Timestamp.makeFromSecondsFloat(this.timestepOvershootSeconds, this.config.timestepSeconds));
}
targetLogicalTimestamp(serverSecondsSinceStartup) {
return Timestamp.makeFromSecondsFloat(serverSecondsSinceStartup, 1 / 60);
}
timestampDriftSeconds(serverSecondsSinceStartup) {
const frameDrift = Timestamp.subFloat(this.currentLogicalTimestamp(), this.targetLogicalTimestamp(serverSecondsSinceStartup));
const secondsDrift = Timestamp.asSeconds(frameDrift, this.config.timestepSeconds);
return secondsDrift;
}
deltaSecondsCompensateForDrift(deltaSeconds, serverSecondsSinceStartup) {
let timestampDriftSeconds;
const drift = this.timestampDriftSeconds(serverSecondsSinceStartup - deltaSeconds);
if (Math.abs(drift) < this.config.timestepSeconds * 0.5) {
// Deadband to avoid oscillating about zero due to floating point precision. The
// absolute time (rather than the delta time) is best used for coarse-grained drift
// compensation.
timestampDriftSeconds = 0;
}
else {
timestampDriftSeconds = drift;
}
const uncappedCompensatedDeltaSeconds = Math.max(deltaSeconds - timestampDriftSeconds, 0);
const compensatedDeltaSeconds =
// Attempted to advance more than the allowed delta seconds. This should not happen too often.
uncappedCompensatedDeltaSeconds > this.config.updateDeltaSecondsMax
? this.config.updateDeltaSecondsMax
: uncappedCompensatedDeltaSeconds;
return compensatedDeltaSeconds;
}
advanceStepper(deltaSeconds) {
let stepCount = 0;
this.timestepOvershootSeconds -= deltaSeconds;
while (true) {
const nextOvershootSeconds = this.timestepOvershootSeconds + this.config.timestepSeconds;
if (shouldTerminate(this.terminationCondition, this.timestepOvershootSeconds, nextOvershootSeconds)) {
break;
}
this.stepper.step();
stepCount++;
this.timestepOvershootSeconds = nextOvershootSeconds;
}
return stepCount;
}
timeskipIfNeeded(serverSecondsSinceStartup) {
const driftSeconds = this.timestampDriftSeconds(serverSecondsSinceStartup);
if (Math.abs(driftSeconds) >= this.config.timestampSkipThresholdSeconds) {
const [correctedTimestamp, correctedOvershootSeconds] = decomposeFloatTimestamp(this.terminationCondition, this.targetLogicalTimestamp(serverSecondsSinceStartup), this.config.timestepSeconds);
this.stepper.resetLastCompletedTimestamp(correctedTimestamp);
this.timestepOvershootSeconds = correctedOvershootSeconds;
}
}
}
//# sourceMappingURL=fixed_timestepper.js.map