UNPKG

lance-gg

Version:

A Node.js based real-time multiplayer game server

168 lines (137 loc) 6.52 kB
import { ClientEngine } from '../ClientEngine.js'; import { GameEngine } from '../GameEngine.js'; import NetworkTransmitter from '../network/NetworkTransmitter.js'; import Serializable from '../serialize/Serializable.js'; interface SyncStrategyOptions {} type Sync = { stepCount: number, fullUpdate: boolean, required: boolean, syncObjects: { [key: number]: Serializable[] }, // all events in the sync indexed by the id of the object involved syncSteps: { [key: number]: { [key: string]: Serializable[] } } // all events in the sync indexed by the step on which they occurred }; class SyncStrategy { protected clientEngine: ClientEngine; protected gameEngine: GameEngine; protected needFirstSync: boolean; private requiredSyncs: Sync[]; protected lastSync: Sync | null; private options: SyncStrategyOptions; static SYNC_APPLIED = 'SYNC_APPLIED'; static STEP_DRIFT_THRESHOLDS = { onServerSync: { MAX_LEAD: 1, MAX_LAG: 3 }, // max step lead/lag allowed after every server sync onEveryStep: { MAX_LEAD: 7, MAX_LAG: 8 }, // max step lead/lag allowed at every step clientReset: 20 // if we are behind this many steps, just reset the step counter }; constructor(inputOptions: SyncStrategyOptions) { this.needFirstSync = true; this.options = Object.assign({}, inputOptions); this.requiredSyncs = []; } initClient(clientEngine: ClientEngine) { this.clientEngine = clientEngine; this.gameEngine = clientEngine.gameEngine; this.gameEngine.on('client__postStep', this.syncStep.bind(this)); this.gameEngine.on('client__syncReceived', this.collectSync.bind(this)); } // collect a sync and its events // maintain a "lastSync" member which describes the last sync we received from // the server. the lastSync object contains: // - syncObjects: all events in the sync indexed by the id of the object involved // - syncSteps: all events in the sync indexed by the step on which they occurred // - objCount // - eventCount // - stepCount collectSync(e) { // on first connect we need to wait for a full world update if (this.needFirstSync) { if (!e.fullUpdate) return; } else { // TODO: there is a problem below in the case where the client is 10 steps behind the server, // and the syncs that arrive are always in the future and never get processed. To address this // we may need to store more than one sync. // ignore syncs which are older than the latest if (this.lastSync && this.lastSync.stepCount && this.lastSync.stepCount > e.stepCount) return; } // before we overwrite the last sync, check if it was a required sync // syncs that create or delete objects are saved because they must be applied. if (this.lastSync && this.lastSync.required) { this.requiredSyncs.push(this.lastSync); } // build new sync object let lastSync: Sync = this.lastSync = { stepCount: e.stepCount, fullUpdate: e.fullUpdate, required: false, syncObjects: {}, syncSteps: {} }; e.syncEvents.forEach(sEvent => { // keep a reference of events by object id if (sEvent.objectInstance) { let objectId = sEvent.objectInstance.id; if (!lastSync.syncObjects[objectId]) lastSync.syncObjects[objectId] = []; lastSync.syncObjects[objectId].push(sEvent); } // keep a reference of events by step let stepCount = sEvent.stepCount; let eventName = NetworkTransmitter.getNetworkEvent(sEvent); if (eventName === 'objectDestroy' || eventName === 'objectCreate') lastSync.required = true; if (!lastSync.syncSteps[stepCount]) lastSync.syncSteps[stepCount] = {}; if (!lastSync.syncSteps[stepCount][eventName]) lastSync.syncSteps[stepCount][eventName] = []; lastSync.syncSteps[stepCount][eventName].push(sEvent); }); let eventCount = e.syncEvents.length; let objCount = (Object.keys(lastSync.syncObjects)).length; let stepCount = (Object.keys(lastSync.syncSteps)).length; this.gameEngine.trace.debug(() => `sync contains ${objCount} objects ${eventCount} events ${stepCount} steps`); } // add an object to our world addNewObject(objId, newObj) { let curObj = new newObj.constructor(this.gameEngine, { id: objId }); // enforce object implementations of syncTo if (!curObj.__proto__.hasOwnProperty('syncTo')) { throw `GameObject of type ${curObj.class} does not implement the syncTo() method, which must copy the netscheme`; } curObj.syncTo(newObj); this.gameEngine.addObjectToWorld(curObj); if (this.clientEngine.options.verbose) console.log(`adding new object ${curObj}`); return curObj; } applySync(sync: Sync, required: boolean): string { return SyncStrategy.SYNC_APPLIED; } // sync to step, by applying bending, and applying the latest sync syncStep(stepDesc) { // apply incremental bending this.gameEngine.world.forEachObject((id, o) => { if (typeof o.applyIncrementalBending === 'function') { o.applyIncrementalBending(stepDesc); o.refreshToPhysics(); } }); // apply all pending required syncs while (this.requiredSyncs.length) { let requiredStep = this.requiredSyncs[0].stepCount; // if we haven't reached the corresponding step, it's too soon to apply syncs if (requiredStep > this.gameEngine.world.stepCount) return; this.gameEngine.trace.trace(() => `applying a required sync ${requiredStep}`); this.applySync(this.requiredSyncs[0], true); this.requiredSyncs.shift(); } // apply the sync and delete it on success if (this.lastSync) { let rc = this.applySync(this.lastSync, false); if (rc === SyncStrategy.SYNC_APPLIED) // TODO: replace above return with a boolean this.lastSync = null; } } } export { SyncStrategy, SyncStrategyOptions, Sync }