UNPKG

@hastearcade/snowglobe

Version:

A TypeScript port of CrystalOrb, a high-level Rust game networking library

115 lines 5.74 kB
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