UNPKG

@geckos.io/snapshot-interpolation

Version:

A Snapshot Interpolation library for Real-Time Multiplayer Games

190 lines 8.23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SnapshotInterpolation = void 0; const vault_1 = require("./vault"); const lerp_1 = require("./lerp"); const slerp_1 = require("./slerp"); /** A Snapshot Interpolation library. */ class SnapshotInterpolation { constructor(serverFPS, config = {}) { /** Access the vault. */ this.vault = new vault_1.Vault(); this._interpolationBuffer = 100; this._timeOffset = -1; /** The current server time based on the current snapshot interpolation. */ this.serverTime = 0; if (serverFPS) this._interpolationBuffer = (1000 / serverFPS) * 3; this.config = { autoCorrectTimeOffset: true, ...config }; } get interpolationBuffer() { return { /** Get the Interpolation Buffer time in milliseconds. */ get: () => this._interpolationBuffer, /** Set the Interpolation Buffer time in milliseconds. */ set: (milliseconds) => { this._interpolationBuffer = milliseconds; } }; } /** Get the current time in milliseconds. */ static Now() { return Date.now(); // - Date.parse('01 Jan 2020') } /** * Get the time offset between client and server (inclusive latency). * If the client and server time are in sync, timeOffset will be the latency. */ get timeOffset() { return this._timeOffset; } /** Create a new ID */ static NewId() { return Math.random().toString(36).substr(2, 6); } get snapshot() { return { /** Create the snapshot on the server. */ create: (state) => SnapshotInterpolation.CreateSnapshot(state), /** Add the snapshot you received from the server to automatically calculate the interpolation with calcInterpolation() */ add: (snapshot) => this.addSnapshot(snapshot) }; } /** Create a new Snapshot */ static CreateSnapshot(state) { const check = (state) => { // check if state is an array if (!Array.isArray(state)) throw new Error('You have to pass an Array to createSnapshot()'); // check if each entity has an id const withoutID = state.filter(e => typeof e.id !== 'string' && typeof e.id !== 'number'); //console.log(withoutID) if (withoutID.length > 0) throw new Error('Each Entity needs to have a id'); }; if (Array.isArray(state)) { check(state); } else { Object.keys(state).forEach(key => { check(state[key]); }); } return { id: SnapshotInterpolation.NewId(), time: SnapshotInterpolation.Now(), state: state }; } addSnapshot(snapshot) { var _a; const timeNow = SnapshotInterpolation.Now(); const timeSnapshot = snapshot.time; if (this._timeOffset === -1) { // the time offset between server and client is calculated, // by subtracting the current client date from the server time of the // first snapshot this._timeOffset = timeNow - timeSnapshot; } // correct time offset if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.autoCorrectTimeOffset) === true) { const timeOffset = timeNow - timeSnapshot; const timeDifference = Math.abs(this._timeOffset - timeOffset); if (timeDifference > 50) this._timeOffset = timeOffset; } this.vault.add(snapshot); } /** Interpolate between two snapshots give the percentage or time. */ interpolate(snapshotA, snapshotB, timeOrPercentage, parameters, deep = '') { return this._interpolate(snapshotA, snapshotB, timeOrPercentage, parameters, deep); } _interpolate(snapshotA, snapshotB, timeOrPercentage, parameters, deep) { const sorted = [snapshotA, snapshotB].sort((a, b) => b.time - a.time); const params = parameters.trim().replace(/\W+/, ' ').split(' '); const newer = sorted[0]; const older = sorted[1]; const t0 = newer.time; const t1 = older.time; /** * If <= it is in percentage * else it is the server time */ const tn = timeOrPercentage; // serverTime is between t0 and t1 // THE TIMELINE // t = time (serverTime) // p = entity position // ------ t1 ------ tn --- t0 ----->> NOW // ------ p1 ------ pn --- p0 ----->> NOW // ------ 0% ------ x% --- 100% --->> NOW const zeroPercent = tn - t1; const hundredPercent = t0 - t1; const pPercent = timeOrPercentage <= 1 ? timeOrPercentage : zeroPercent / hundredPercent; this.serverTime = (0, lerp_1.lerp)(t1, t0, pPercent); const lerpFnc = (method, start, end, t) => { if (typeof start === 'undefined' || typeof end === 'undefined') return; if (typeof start === 'string' || typeof end === 'string') throw new Error(`Can't interpolate string!`); if (typeof start === 'number' && typeof end === 'number') { if (method === 'linear') return (0, lerp_1.lerp)(start, end, t); else if (method === 'deg') return (0, lerp_1.degreeLerp)(start, end, t); else if (method === 'rad') return (0, lerp_1.radianLerp)(start, end, t); } if (typeof start === 'object' && typeof end === 'object') { if (method === 'quat') return (0, slerp_1.quatSlerp)(start, end, t); } throw new Error(`No lerp method "${method}" found!`); }; if (!Array.isArray(newer.state) && deep === '') throw new Error('You forgot to add the "deep" parameter.'); if (Array.isArray(newer.state) && deep !== '') throw new Error('No "deep" needed it state is an array.'); const newerState = Array.isArray(newer.state) ? newer.state : newer.state[deep]; const olderState = Array.isArray(older.state) ? older.state : older.state[deep]; let tmpSnapshot = JSON.parse(JSON.stringify({ ...newer, state: newerState })); newerState.forEach((e, i) => { const id = e.id; const other = olderState.find((e) => e.id === id); if (!other) return; params.forEach(p => { // TODO yandeu: improve this code const match = p.match(/\w\(([\w]+)\)/); const lerpMethod = match ? match === null || match === void 0 ? void 0 : match[1] : 'linear'; if (match) p = match === null || match === void 0 ? void 0 : match[0].replace(/\([\S]+$/gm, ''); const p0 = e === null || e === void 0 ? void 0 : e[p]; const p1 = other === null || other === void 0 ? void 0 : other[p]; const pn = lerpFnc(lerpMethod, p1, p0, pPercent); if (Array.isArray(tmpSnapshot.state)) tmpSnapshot.state[i][p] = pn; }); }); const interpolatedSnapshot = { state: tmpSnapshot.state, percentage: pPercent, newer: newer.id, older: older.id }; return interpolatedSnapshot; } /** Get the calculated interpolation on the client. */ calcInterpolation(parameters, deep = '') { // get the snapshots [this._interpolationBuffer] ago const serverTime = SnapshotInterpolation.Now() - this._timeOffset - this._interpolationBuffer; const shots = this.vault.get(serverTime); if (!shots) return; const { older, newer } = shots; if (!older || !newer) return; return this._interpolate(newer, older, serverTime, parameters, deep); } } exports.SnapshotInterpolation = SnapshotInterpolation; //# sourceMappingURL=snapshot-interpolation.js.map