UNPKG

incheon

Version:

A Node.js based real-time game server

205 lines (163 loc) 7.42 kB
'use strict'; const SyncStrategy = require('./SyncStrategy'); const defaults = { syncsBufferLength: 6, clientStepHold: 6, reflect: false }; class InterpolateStrategy extends SyncStrategy { constructor(clientEngine, inputOptions) { const options = Object.assign({}, defaults, inputOptions); super(clientEngine, options); this.syncsBuffer = []; // buffer for server world updates this.gameEngine = this.clientEngine.gameEngine; this.gameEngine.passive = true; // client side engine ignores inputs this.gameEngine.on('client__postStep', this.interpolate.bind(this)); this.gameEngine.on('client__syncReceived', this.collectSync.bind(this)); } collectSync(e) { // TODO the event sorting code below is used in one way or another // by interpolate, extrapolate and reflect. Consider placing // it in the base class. let lastSync = this.lastSync = {}; lastSync.stepCount = e.stepCount; // keep a reference of events by object id lastSync.syncObjects = {}; e.syncEvents.forEach(sEvent => { let o = sEvent.objectInstance; if (!o) return; if (!lastSync.syncObjects[o.id]) { lastSync.syncObjects[o.id] = []; } lastSync.syncObjects[o.id].push(sEvent); }); // keep a reference of events by step lastSync.syncSteps = {}; e.syncEvents.forEach(sEvent => { // add an entry for this step and event-name if (!lastSync.syncSteps[sEvent.stepCount]) lastSync.syncSteps[sEvent.stepCount] = {}; if (!lastSync.syncSteps[sEvent.stepCount][sEvent.eventName]) lastSync.syncSteps[sEvent.stepCount][sEvent.eventName] = []; lastSync.syncSteps[sEvent.stepCount][sEvent.eventName].push(sEvent); }); let objCount = (Object.keys(lastSync.syncObjects)).length; let eventCount = e.syncEvents.length; let stepCount = (Object.keys(lastSync.syncSteps)).length; this.gameEngine.trace.debug(`sync contains ${objCount} objects ${eventCount} events ${stepCount} steps`); this.syncsBuffer.push(lastSync); if (this.syncsBuffer.length >= this.options.syncsBufferLength) { this.syncsBuffer.shift(); } } // add an object to our world addNewObject(objId, newObj, stepCount) { let curObj = new newObj.constructor(); curObj.syncTo(newObj); curObj.passive = true; this.gameEngine.addObjectToWorld(curObj); console.log(`adding new object ${curObj}`); // if this game keeps a physics engine on the client side, // we need to update it as well if (this.gameEngine.physicsEngine && typeof curObj.initPhysicsObject === 'function') { curObj.initPhysicsObject(this.gameEngine.physicsEngine); } if (stepCount) { curObj.lastUpdateStep = stepCount; } return curObj; } /** * Perform client-side interpolation. */ interpolate() { // get the step we will perform let world = this.gameEngine.world; let stepToPlay = world.stepCount - this.options.clientStepHold; let nextSync = null; // get the closest sync to our next step for (let x = 0; x < this.syncsBuffer.length; x++) { if (this.syncsBuffer[x].stepCount >= stepToPlay) { nextSync = this.syncsBuffer[x]; break; } } // we requires a sync before we proceed if (!nextSync) { this.gameEngine.trace.debug('interpolate lacks future sync - requesting step skip'); this.clientEngine.skipOneStep = true; return; } this.gameEngine.trace.debug(`interpolate past step [${stepToPlay}] using sync from step ${nextSync.stepCount}`); // create objects which are created at this step let stepEvents = nextSync.syncSteps[stepToPlay]; if (stepEvents && stepEvents.objectCreate) { stepEvents.objectCreate.forEach(ev => { this.addNewObject(ev.objectInstance.id, ev.objectInstance, stepToPlay); }); } // create objects for events that imply a create-object if (stepEvents && stepEvents.objectUpdate) { stepEvents.objectUpdate.forEach(ev => { if (!world.objects[ev.objectInstance.id]) this.addNewObject(ev.objectInstance.id, ev.objectInstance, stepToPlay); }); } // remove objects which are removed at this step if (stepEvents && stepEvents.objectDestroy) { stepEvents.objectDestroy.forEach(ev => { if (world.objects[ev.objectInstance.id]) this.gameEngine.removeObjectFromWorld(ev.objectInstance.id); }); } // interpolate values for all objects in this world world.forEachObject((id, ob) => { let nextObj = null; let nextStep = null; // if we already handled this object, continue // TODO maybe call it lastUpdatedStep if (ob.lastUpdateStep === stepToPlay) return; // get the nearest object we can interpolate to if (!nextSync.syncObjects.hasOwnProperty(id)) return; nextSync.syncObjects[id].forEach(ev => { if (!nextObj && ev.stepCount >= stepToPlay) { nextObj = ev.objectInstance; nextStep = ev.stepCount; } }); if (nextObj) { let playPercentage = 1 / (nextStep + 1 - stepToPlay); if (this.options.reflect) playPercentage = 1.0; this.interpolateOneObject(ob, nextObj, id, playPercentage); } }); // destroy objects world.forEachObject((id, ob) => { let objEvents = nextSync.syncObjects[id]; if (!objEvents || Number(id) >= this.gameEngine.options.clientIDSpace) return; objEvents.forEach((e) => { if (e.eventName === 'objectDestroy') this.gameEngine.removeObjectFromWorld(id); }); }); } // TODO: prevObj is now just curObj // and playPercentage is 1/(nextObj.step - now) // so the code below should be easy to simplify now interpolateOneObject(prevObj, nextObj, objId, playPercentage) { // handle step for this object let curObj = this.gameEngine.world.objects[objId]; if (typeof curObj.interpolate === 'function') { // update positions with interpolation this.gameEngine.trace.trace(`object ${objId} before ${playPercentage} interpolate: ${curObj.toString()}`); curObj.interpolate(nextObj, playPercentage, this.gameEngine.worldSettings); this.gameEngine.trace.trace(`object ${objId} after interpolate: ${curObj.toString()}`); // if this object has a physics sub-object, it must inherit // the position now. if (curObj.physicalObject && typeof curObj.updatePhysicsObject === 'function') { curObj.updatePhysicsObject(); } } } } module.exports = InterpolateStrategy;