@geckos.io/snapshot-interpolation
Version:
A Snapshot Interpolation library for Real-Time Multiplayer Games
190 lines • 8.23 kB
JavaScript
;
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