diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..af0f0c3 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c9afd91..08bb8cc 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,31 +2,41 @@ # Release Notes for Lance -## Release 1.0.1 - -### Breaking Changes - -1. Event `preInput` was renamed to `processInput`, `client__processInput`, `server__processInput`. `postInput`. This is a breaking change but no one actually used these events. - -## Release 1.0.10 - -### Breaking Change - -1. fix a breaking change that was introduced in 1.0.9 - which moved the initialization from the physics engine into the GameEngine - -## Release 2.0.0 - June 2017 +## Release 2.0.0 - February 2018 ### New Features * new netscheme data type: *STRING*. Will only be broadcast if it changed since last broadcast. * PhysicsEngine no longer initialized in two places. It is initialized in the GameEngine - +* Implemented HSHG collision detection for SimplePhysics +* Implemented ClientEngine standaloneMode for network-less testing of game engines +* New KeyboardControls class to help with sending key-based input +* Moved to es6 modules on all games +* ES6 Modules: Modules are imported using the "import from" construct. For example import GameEngine from 'lance/GameEngine' instead of const GameEngine = require(...) +* ES6 Modules: Games must configure webpack.config.js. See sample game +* ES6 Modules: Babel must be configured in order to run es6 modules on node server-side, by creating a .babelrc file. See sample game +* Renderer-controlled game loop support: the physics engine is asked to step forwards by dt, where dt is the time between current frame render and last frame render +* full-sync support for updating all game objects when new player connects +* Renderer refactored as a singleton, and instantiated only on client +* Render objects and Physics objects are now sub-objects of the GameObject ### Breaking Changes -* `PhysicsEngine` should no longer be instantiated in the Server `main.js` and in the client entry point. Rather, it should be instantiated in the `GameEngine`. +* All classes are now in ES6 format instead of CommonJS +* `PhysicsEngine` should no longer be instantiated in the Server `main.js` and in the client entry point. Rather, it should be instantiated in the `GameEngine` subclass for your game. +* `PhysicsEngine` constructor now does all initialization. Use of `init` function is deprecated. * `GameEngine` step method cannot be called without passing the `isReenact` argument. Games which override the `step` method must pass this argument when calling the super method. +* `GameObject` New `onRemoveFromWorld` to mirror `onAddToWorld` +* Objects are now instantiated with a reference to the gameEngine, and get and ID automatically +* Method `isOwnedByPlayer` moved from `clientEngine` to `GameEngine`, and the `clientEngine` now sets the `playerId` in the `gameEngine`. `GameObject` constructor is therefore: constructor(gameEngine, options, props) and must call the super constructor correspondingly +* The `GameWorld.getPlayerObject()` method has been removed, you can get the player objects using the `GameWorld.query()` method, passing a `playerId` attribute. +* constructors of `DynamicObject` and `PhysicalObject` have changed to the following: gameEngine, options, and props. + +## Release 1.0.1 +### Breaking Changes + +1. Event `preInput` was renamed to `processInput`, `client__processInput`, `server__processInput`. `postInput`. This is a breaking change but no one actually used these events. ## Release 1.0.0 - March 2017 diff --git a/docs/spaceships.md b/docs/spaceships.md index 9811131..6afed5b 100644 --- a/docs/spaceships.md +++ b/docs/spaceships.md @@ -228,7 +228,7 @@ will be ignored. A trace message is usually recorded as follows: ```javascript -gameEngine.trace.info(`this just happened: ${foobar()}`); +gameEngine.trace.info(() => `this just happened: ${foobar()}`); ``` By default, Lance already traces a lot of information, describing diff --git a/jsdoc.conf.json b/jsdoc.conf.json index 54a847e..6a659ef 100644 --- a/jsdoc.conf.json +++ b/jsdoc.conf.json @@ -11,6 +11,7 @@ "src/ServerEngine.js", "src/ClientEngine.js", "src/GameEngine.js", + "src/serialize/GameObject.js", "src/serialize/DynamicObject.js", "src/serialize/Serializer.js", "src/serialize/PhysicalObject.js", @@ -19,7 +20,7 @@ "src/serialize/Quaternion.js", "src/render/Renderer.js", "src/render/AFrameRenderer.js", - "src/matchMaker/MatchMaker.js" + "src/lib/Trace.js" ] }, "plugins": [ diff --git a/main.js b/main.js deleted file mode 100644 index 676d187..0000000 --- a/main.js +++ /dev/null @@ -1,32 +0,0 @@ -var Lance = { - GameEngine: require('./src/GameEngine'), - ServerEngine: require('./src/ServerEngine'), - ClientEngine: require('./src/ClientEngine'), - Synchronizer: require('./src/Synchronizer'), - GameWorld: require('./src/GameWorld'), - serialize: { - Serializer: require('./src/serialize/Serializer'), - Serializable: require('./src/serialize/Serializable'), - PhysicalObject: require('./src/serialize/PhysicalObject'), - THREEPhysicalObject: require('./src/serialize/THREEPhysicalObject'), - DynamicObject: require('./src/serialize/DynamicObject'), - ThreeVector: require('./src/serialize/ThreeVector'), - TwoVector: require('./src/serialize/TwoVector'), - Quaternion: require('./src/serialize/Quaternion') - }, - physics: { - PhysicsEngine: require('./src/physics/PhysicsEngine'), - SimplePhysicsEngine: require('./src/physics/SimplePhysicsEngine'), - CannonPhysicsEngine: require('./src/physics/CannonPhysicsEngine') - }, - render: { - Renderer: require('./src/render/Renderer'), - ThreeRenderer: require('./src/render/ThreeRenderer'), - AFrameRenderer: require('./src/render/AFrameRenderer') - }, - controls: { - Keyboard: require('./src/controls/KeyboardControls'), - } -}; - -module.exports = Lance; diff --git a/package.json b/package.json index 82d439e..098c585 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lance-gg", - "version": "1.0.10", + "version": "2.0.0", "description": "A Node.js based real-time multiplayer game server", "keywords": [ "pixijs", @@ -25,7 +25,9 @@ "socket.io": "^1.4.6", "socket.io-client": "^1.4.6" }, - "main": "main.js", + "files": [ + "src" + ], "author": "Opher Vishnia", "contributors": [ { @@ -44,9 +46,10 @@ "docs": "jsdoc -c jsdoc.conf.json", "test-all": "mocha test/EndToEnd/", "test-serializer": "mocha ./test/serializer/", - "test": "mocha ./test/serializer/" + "test": "mocha ./test/serializer/ --compilers js:babel-core/register" }, "devDependencies": { + "babel-core": "^6.25.0", "babel-preset-es2015": "^6.22.0", "chai": "^3.5.0", "docmeta": "OpherV/docmeta", diff --git a/src/ClientEngine.js b/src/ClientEngine.js index e16f651..cca0130 100644 --- a/src/ClientEngine.js +++ b/src/ClientEngine.js @@ -1,17 +1,16 @@ -'use strict'; -let io = require('socket.io-client'); -const Utils = require('./lib/Utils'); -const Scheduler = require('./lib/Scheduler'); -const Synchronizer = require('./Synchronizer'); -const Serializer = require('./serialize/Serializer'); -const NetworkMonitor = require('./network/NetworkMonitor'); -const NetworkTransmitter = require('./network/NetworkTransmitter'); +import io from 'socket.io-client'; +import Utils from './lib/Utils'; +import Scheduler from './lib/Scheduler'; +import Synchronizer from './Synchronizer'; +import Serializer from './serialize/Serializer'; +import NetworkMonitor from './network/NetworkMonitor'; +import NetworkTransmitter from './network/NetworkTransmitter'; // externalizing these parameters as options would add confusion to game // developers, and provide no real benefit. const STEP_DRIFT_THRESHOLDS = { onServerSync: { MAX_LEAD: 1, MAX_LAG: 3 }, // max step lead/lag allowed after every server sync - onEveryStep: { MAX_LEAD: 10, MAX_LAG: 10 } // max step lead/lag allowed at every step + onEveryStep: { MAX_LEAD: 7, MAX_LAG: 8 } // max step lead/lag allowed at every step }; const STEP_DRIFT_THRESHOLD__CLIENT_RESET = 20; // if we are behind this many steps, just reset the step counter const GAME_UPS = 60; // default number of game steps per second @@ -24,7 +23,7 @@ const STEP_HURRY_MSEC = 8; // if backward drift detected, hurry next execution b * starting client steps, and handling world updates which arrive from * the server. */ -class ClientEngine { +export default class ClientEngine { /** * Create a client engine instance. @@ -32,6 +31,7 @@ class ClientEngine { * @param {GameEngine} gameEngine - a game engine * @param {Object} inputOptions - options object * @param {Boolean} inputOptions.autoConnect - if true, the client will automatically attempt connect to server. + * @param {Boolean} inputOptions.standaloneMode - if true, the client will never try to connect to a server * @param {Number} inputOptions.delayInputCount - if set, inputs will be delayed by this many steps before they are actually applied on the client. * @param {Number} inputOptions.healthCheckInterval - health check message interval (millisec). Default is 1000. * @param {Number} inputOptions.healthCheckRTTSample - health check RTT calculation sample size. Default is 10. @@ -49,7 +49,7 @@ class ClientEngine { healthCheckInterval: 1000, healthCheckRTTSample: 10, stepPeriod: 1000 / GAME_UPS, - scheduler: 'fixed' + scheduler: 'fixed', }, inputOptions); /** @@ -77,14 +77,10 @@ class ClientEngine { this.scheduler = null; this.lastStepTime = 0; this.correction = 0; - - /** - * client's player ID, as a string. - * @member {String} - */ - this.playerId = NaN; - - this.configureSynchronization(); + + if (this.options.standaloneMode !== true) { + this.configureSynchronization(); + } // create a buffer of delayed inputs (fifo) if (inputOptions && inputOptions.delayInputCount) { @@ -94,25 +90,16 @@ class ClientEngine { } } - /** - * Check if a given object is owned by the player on this client - * - * @param {Object} object the game object to check - * @return {Boolean} true if the game object is owned by the player on this client - */ - isOwnedByPlayer(object) { - return (object.playerId == this.playerId); - } - configureSynchronization() { // the reflect syncronizer is just interpolate strategy, // configured to show server syncs let syncOptions = this.options.syncOptions; - if (syncOptions.sync === 'reflect') { + if (syncOptions.sync === 'reflect') { //todo would be nicer to use an enum syncOptions.sync = 'interpolate'; syncOptions.reflect = true; } + const synchronizer = new Synchronizer(this, syncOptions); } @@ -124,33 +111,32 @@ class ClientEngine { */ connect(options = {}) { - let that = this; - function connectSocket(matchMakerAnswer) { + let connectSocket = matchMakerAnswer => { return new Promise((resolve, reject) => { if (matchMakerAnswer.status !== 'ok') reject(); console.log(`connecting to game server ${matchMakerAnswer.serverURL}`); - that.socket = io(matchMakerAnswer.serverURL, options); + this.socket = io(matchMakerAnswer.serverURL, options); - that.networkMonitor.registerClient(that); + this.networkMonitor.registerClient(this); - that.socket.once('connect', () => { + this.socket.once('connect', () => { console.log('connection made'); resolve(); }); - that.socket.on('playerJoined', (playerData) => { - that.playerId = playerData.playerId; - that.messageIndex = Number(that.playerId) * 10000; + this.socket.on('playerJoined', (playerData) => { + this.gameEngine.playerId = playerData.playerId; + this.messageIndex = Number(this.gameEngine.playerId) * 10000; //todo magic number }); - that.socket.on('worldUpdate', (worldData) => { - that.inboundMessages.push(worldData); + this.socket.on('worldUpdate', (worldData) => { + this.inboundMessages.push(worldData); }); }); - } + }; let matchmaker = Promise.resolve({ serverURL: null, status: 'ok' }); if (this.options.matchmaker) @@ -166,30 +152,30 @@ class ClientEngine { * ready to connect */ start() { - - this.gameEngine.start(); - if (this.options.scheduler === 'fixed') { - // schedule and start the game loop - this.scheduler = new Scheduler({ - period: this.options.stepPeriod, - tick: this.step.bind(this), - delay: STEP_DELAY_MSEC - }); - this.scheduler.start(); - } - // initialize the renderer // the render loop waits for next animation frame if (!this.renderer) alert('ERROR: game has not defined a renderer'); - let renderLoop = () => { - this.renderer.draw(); + let renderLoop = (timestamp) => { + this.renderer.draw(timestamp); window.requestAnimationFrame(renderLoop); }; return this.renderer.init().then(() => { + this.gameEngine.start(); + + if (this.options.scheduler === 'fixed') { + // schedule and start the game loop + this.scheduler = new Scheduler({ + period: this.options.stepPeriod, + tick: this.step.bind(this), + delay: STEP_DELAY_MSEC + }); + this.scheduler.start(); + } + if (typeof window !== 'undefined') window.requestAnimationFrame(renderLoop); - if (this.options.autoConnect) { + if (this.options.autoConnect && this.options.standaloneMode !== true) { this.connect(); } }); @@ -207,12 +193,12 @@ class ClientEngine { let clientStep = this.gameEngine.world.stepCount; let serverStep = this.gameEngine.serverStep; if (clientStep > serverStep + maxLead) { - this.gameEngine.trace.warn(`step drift ${checkType}. [${clientStep} > ${serverStep} + ${maxLead}] Client is ahead of server. Delaying next step.`); + this.gameEngine.trace.warn(() => `step drift ${checkType}. [${clientStep} > ${serverStep} + ${maxLead}] Client is ahead of server. Delaying next step.`); if (this.scheduler) this.scheduler.delayTick(); this.lastStepTime += STEP_DELAY_MSEC; this.correction += STEP_DELAY_MSEC; } else if (serverStep > clientStep + maxLag) { - this.gameEngine.trace.warn(`step drift ${checkType}. [${serverStep} > ${clientStep} + ${maxLag}] Client is behind server. Hurrying next step.`); + this.gameEngine.trace.warn(() => `step drift ${checkType}. [${serverStep} > ${clientStep} + ${maxLag}] Client is behind server. Hurrying next step.`); if (this.scheduler) this.scheduler.hurryTick(); this.lastStepTime -= STEP_HURRY_MSEC; this.correction -= STEP_HURRY_MSEC; @@ -246,12 +232,14 @@ class ClientEngine { this.checkDrift('onEveryStep'); // perform game engine step - this.handleOutboundInput(); + if (this.options.standaloneMode !== true) { + this.handleOutboundInput(); + } this.applyDelayedInputs(); this.gameEngine.step(false, t, dt); - this.gameEngine.emit('client__postStep'); + this.gameEngine.emit('client__postStep', { dt }); - if (this.gameEngine.trace.length && this.socket) { + if (this.options.standaloneMode !== true && this.gameEngine.trace.length && this.socket) { // socket might not have been initialized at this point this.socket.emit('trace', JSON.stringify(this.gameEngine.trace.rotate())); } @@ -262,10 +250,10 @@ class ClientEngine { return; } - const inputEvent = { input: message.data, playerId: this.playerId }; + const inputEvent = { input: message.data, playerId: this.gameEngine.playerId }; this.gameEngine.emit('client__processInput', inputEvent); this.gameEngine.emit('processInput', inputEvent); - this.gameEngine.processInput(message.data, this.playerId, false); + this.gameEngine.processInput(message.data, this.gameEngine.playerId, false); } @@ -304,7 +292,7 @@ class ClientEngine { } }; - this.gameEngine.trace.info(`USER INPUT[${this.messageIndex}]: ${input} ${inputOptions ? JSON.stringify(inputOptions) : '{}'}`); + this.gameEngine.trace.info(() => `USER INPUT[${this.messageIndex}]: ${input} ${inputOptions ? JSON.stringify(inputOptions) : '{}'}`); // if we delay input application on client, then queue it // otherwise apply it now @@ -313,7 +301,10 @@ class ClientEngine { } else { this.doInputLocal(message); } - this.outboundMessages.push(message); + + if (this.options.standaloneMode !== true) { + this.outboundMessages.push(message); + } this.messageIndex++; } @@ -331,11 +322,11 @@ class ClientEngine { fullUpdate: syncHeader.fullUpdate }); - this.gameEngine.trace.info(`========== inbound world update ${syncHeader.stepCount} ==========`); + this.gameEngine.trace.info(() => `========== inbound world update ${syncHeader.stepCount} ==========`); // finally update the stepCount if (syncHeader.stepCount > this.gameEngine.world.stepCount + STEP_DRIFT_THRESHOLD__CLIENT_RESET) { - this.gameEngine.trace.info(`========== world step count updated from ${this.gameEngine.world.stepCount} to ${syncHeader.stepCount} ==========`); + this.gameEngine.trace.info(() => `========== world step count updated from ${this.gameEngine.world.stepCount} to ${syncHeader.stepCount} ==========`); this.gameEngine.emit('client__stepReset', { oldStep: this.gameEngine.world.stepCount, newStep: syncHeader.stepCount }); this.gameEngine.world.stepCount = syncHeader.stepCount; } @@ -349,5 +340,3 @@ class ClientEngine { } } - -module.exports = ClientEngine; diff --git a/src/GameEngine.js b/src/GameEngine.js index 3c4a07b..0055bef 100644 --- a/src/GameEngine.js +++ b/src/GameEngine.js @@ -1,7 +1,7 @@ -'use strict'; -const GameWorld = require('./GameWorld'); -const EventEmitter = require('eventemitter3'); -const Trace = require('./lib/Trace'); +import GameWorld from './GameWorld'; +import EventEmitter from 'eventemitter3'; +import Timer from './game/Timer'; +import Trace from './lib/Trace'; /** * The GameEngine contains the game logic. Extend this class @@ -22,7 +22,7 @@ const Trace = require('./lib/Trace'); * and therefore clients must resolve server updates which conflict * with client-side predictions. */ -class GameEngine { +export default class GameEngine { /** * Create a game engine instance. This needs to happen @@ -34,21 +34,22 @@ class GameEngine { */ constructor(options) { + // TODO I think we should discuss this whole globals issues // place the game engine in the LANCE globals - const glob = (typeof window === 'undefined') ? global : window; + const isServerSide = (typeof window === 'undefined'); + const glob = isServerSide ? global : window; glob.LANCE = { gameEngine: this }; - // if no GameWorld is specified, use the default one - this.options = Object.assign({ - GameWorld: GameWorld, - traceLevel: Trace.TRACE_NONE - }, options); + // set options + const defaultOpts = { GameWorld: GameWorld, traceLevel: Trace.TRACE_NONE }; + if (!isServerSide) defaultOpts.clientIDSpace = 1000000; + this.options = Object.assign(defaultOpts, options); - // get the physics engine and initialize it - if (this.options.physicsEngine) { - this.physicsEngine = this.options.physicsEngine; - this.physicsEngine.init({ gameEngine: this }); - } + /** + * client's player ID, as a string. If running on the client, this is set at runtime by the clientEngine + * @member {String} + */ + this.playerId = NaN; // set up event emitting and interface let eventEmitter = new EventEmitter(); @@ -104,7 +105,7 @@ class GameEngine { return null; } - initWorld() { + initWorld(worldSettings) { this.world = new GameWorld(); @@ -120,7 +121,7 @@ class GameEngine { * @member {Object} worldSettings * @memberof GameEngine */ - this.worldSettings = {}; + this.worldSettings = Object.assign({}, worldSettings); } /** @@ -130,9 +131,17 @@ class GameEngine { * and registering methods on the event handler. */ start() { - this.trace.info('========== game engine started =========='); + this.trace.info(() => '========== game engine started =========='); this.initWorld(); - this.emit('server__start', { timestamp: (new Date()).getTime() }); + + // create the default timer + this.timer = new Timer(); + this.timer.play(); + this.on('postStep', (step, isReenact) => { + if (!isReenact) this.timer.tick(); + }); + + this.emit('start', { timestamp: (new Date()).getTime() }); } /** @@ -144,7 +153,6 @@ class GameEngine { * @param {Boolean} physicsOnly - do a physics step only, no game logic */ step(isReenact, t, dt, physicsOnly) { - // physics-only step if (physicsOnly) { if (dt) dt /= 1000; // physics engines work in seconds @@ -178,7 +186,7 @@ class GameEngine { this.world.forEachObject((id, o) => { if (typeof o.refreshFromPhysics === 'function') o.refreshFromPhysics(); - this.trace.trace(`object[${id}] after ${isReenact ? 'reenact' : 'step'} : ${o.toString()}`); + this.trace.trace(() => `object[${id}] after ${isReenact ? 'reenact' : 'step'} : ${o.toString()}`); }); // emit postStep event @@ -205,12 +213,12 @@ class GameEngine { serverCopyArrived = true; }); if (serverCopyArrived) { - this.trace.info(`========== shadow object NOT added ${object.toString()} ==========`); + this.trace.info(() => `========== shadow object NOT added ${object.toString()} ==========`); return null; } } - this.world.objects[object.id] = object; + this.world.addObject(object); // tell the object to join the game, by creating // its corresponding physical entities and renderer entities. @@ -218,7 +226,7 @@ class GameEngine { object.onAddToWorld(this); this.emit('objectAdded', object); - this.trace.info(`========== object added ${object.toString()} ==========`); + this.trace.info(() => `========== object added ${object.toString()} ==========`); return object; } @@ -244,22 +252,39 @@ class GameEngine { * @param {Boolean} isServer - indicate if this function is being called on the server side */ processInput(inputMsg, playerId, isServer) { - this.trace.info(`game engine processing input[${inputMsg.messageIndex}] <${inputMsg.input}> from playerId ${playerId}`); + this.trace.info(() => `game engine processing input[${inputMsg.messageIndex}] <${inputMsg.input}> from playerId ${playerId}`); } /** * Remove an object from the game world. * - * @param {String} id - the object ID + * @param {Object|String} objectId - the object or object ID + */ + removeObjectFromWorld(objectId) { + + if (typeof objectId === 'object') objectId = objectId.id; + let object = this.world.objects[objectId]; + + if (!object) { + throw new Error(`Game attempted to remove a game object which doesn't (or never did) exist, id=${objectId}`); + } + this.trace.info(() => `========== destroying object ${object.toString()} ==========`); + + if (typeof object.onRemoveFromWorld === 'function') + object.onRemoveFromWorld(this); + + this.emit('objectDestroyed', object); + this.world.removeObject(objectId); + } + + /** + * Check if a given object is owned by the player on this client + * + * @param {Object} object the game object to check + * @return {Boolean} true if the game object is owned by the player on this client */ - removeObjectFromWorld(id) { - let ob = this.world.objects[id]; - if (!ob) - throw new Error(`Game attempted to remove a game object which doesn't (or never did) exist, id=${id}`); - this.trace.info(`========== destroying object ${ob.toString()} ==========`); - this.emit('objectDestroyed', ob); - ob.destroy(); - delete this.world.objects[id]; + isOwnedByPlayer(object) { + return (object.playerId == this.playerId); } /** @@ -461,8 +486,6 @@ class GameEngine { /** * server has started * - * @event GameEngine#server__start + * @event GameEngine#start * @param {Number} timestamp - UTC epoch of start time */ - -module.exports = GameEngine; diff --git a/src/GameWorld.js b/src/GameWorld.js index dc9acf7..604b3d6 100644 --- a/src/GameWorld.js +++ b/src/GameWorld.js @@ -1,11 +1,9 @@ -'use strict'; - /** * This class represents an instance of the game world, * where all data pertaining to the current state of the * world is saved. */ -class GameWorld { +export default class GameWorld { /** * Constructor of the World instance @@ -18,46 +16,107 @@ class GameWorld { } /** - * World object iterator. - * Invoke callback(objId, obj) for each object - * - * @param {function} callback function receives id and object + * Gets a new, fresh and unused id that can be used for a new object + * @return {Number} the new id */ - forEachObject(callback) { - for (let id of Object.keys(this.objects)) - callback(id, this.objects[id]); // TODO: the key should be Number(id) + getNewId() { + let possibleId = this.idCount; + // find a free id + while (possibleId in this.objects) + possibleId++; + + this.idCount = possibleId + 1; + return possibleId; } /** - * Return the primary game object for a specific player - * - * @param {Number} playerId the player ID - * @return {Object} game object for this player + * Returns all the game world objects which match a criteria + * @param {Object} query The query object + * @param {Object} [query.id] object id + * @param {Object} [query.playerId] player id + * @param {Class} [query.instanceType] matches whether `object instanceof instanceType` + * @param {Array} [query.components] An array of component names + * @param {Boolean} [query.returnSingle] Return the first object matched + * @returns {Array | Object} All game objects which match all the query parameters, or the first match if returnSingle was specified */ - getPlayerObject(playerId) { - for (let objId of Object.keys(this.objects)) { - let o = this.objects[objId]; - if (o.playerId === playerId) - return o; + queryObjects(query) { + let queriedObjects = []; + + // todo this is currently a somewhat inefficient implementation for API testing purposes. + // It should be implemented with cached dictionaries like in nano-ecs + this.forEachObject((id, object) => { + let conditions = []; + + // object id condition + conditions.push(!('id' in query) || query.id && object.id === query.id); + + // player id condition + conditions.push(!('playerId' in query) || query.playerId && object.playerId === query.playerId); + + // instance type conditio + conditions.push(!('instanceType' in query) || query.instanceType && object instanceof query.instanceType); + + // components conditions + if ('components' in query) { + query.components.forEach(componentClass => { + conditions.push(object.hasComponent(componentClass)); + }); + } + + // all conditions are true, object is qualified for the query + if (conditions.every(value => value)) { + queriedObjects.push(object); + if (query.returnSingle) return false; + } + }); + + // return a single object or null + if (query.returnSingle) { + return queriedObjects.length > 0 ? queriedObjects[0] : null; } + + return queriedObjects; } /** - * Return an array of all the game objects owned by a specific player + * Returns The first game object encountered which matches a criteria. + * Syntactic sugar for {@link queryObject} with `returnSingle: true` + * @param query See queryObjects + * @returns {Object} + */ + queryObject(query) { + return this.queryObjects(Object.assign(query, { + returnSingle: true + })); + } + + /** + * Remove an object from the game world + * @param object + */ + addObject(object) { + this.objects[object.id] = object; + } + + /** + * Add an object to the game world + * @param id + */ + removeObject(id) { + delete this.objects[id]; + } + + /** + * World object iterator. + * Invoke callback(objId, obj) for each object * - * @param {Number} playerId the player ID - * @return {Array} game objects owned by this player + * @param {function} callback function receives id and object. If callback returns false, the iteration will cease */ - getOwnedObject(playerId) { - let owned = []; - for (let objId of Object.keys(this.objects)) { - let o = this.objects[objId]; - if (o.ownerId === playerId) - owned.push(o); + forEachObject(callback) { + for (let id of Object.keys(this.objects)) { + let returnValue = callback(id, this.objects[id]); // TODO: the key should be Number(id) + if (returnValue === false) break; } - return owned; } } - -module.exports = GameWorld; diff --git a/src/ServerEngine.js b/src/ServerEngine.js index 31ef79b..e17b032 100644 --- a/src/ServerEngine.js +++ b/src/ServerEngine.js @@ -1,11 +1,11 @@ 'use strict'; -const fs = require('fs'); -const Utils = require('./lib/Utils'); -const Scheduler = require('./lib/Scheduler'); -const Serializer = require('./serialize/Serializer'); -const NetworkTransmitter = require('./network/NetworkTransmitter'); -const NetworkMonitor = require('./network/NetworkMonitor'); +import fs from 'fs'; +import Utils from './lib/Utils'; +import Scheduler from './lib/Scheduler'; +import Serializer from './serialize/Serializer'; +import NetworkTransmitter from './network/NetworkTransmitter'; +import NetworkMonitor from './network/NetworkMonitor'; /** * ServerEngine is the main server-side singleton code. @@ -24,7 +24,7 @@ const NetworkMonitor = require('./network/NetworkMonitor'); * connections and dis-connections, emitting periodic game-state * updates, and capturing remote user inputs. */ -class ServerEngine { +export default class ServerEngine { /** * create a ServerEngine instance @@ -139,7 +139,7 @@ class ServerEngine { } let payload = this.serializeUpdate({ diffUpdate }); - this.gameEngine.trace.info(`========== sending world update ${this.gameEngine.world.stepCount} is delta update: ${diffUpdate} ==========`); + this.gameEngine.trace.info(() => `========== sending world update ${this.gameEngine.world.stepCount} is delta update: ${diffUpdate} ==========`); // TODO: implement server lag by queuing the emit to a future step for (let socketId of Object.keys(this.connectedPlayers)) this.connectedPlayers[socketId].socket.emit('worldUpdate', payload); @@ -208,7 +208,6 @@ class ServerEngine { // handle the object creation onObjectAdded(obj) { - console.log('object created event'); this.networkTransmitter.addNetworkedEvent('objectCreate', { stepCount: this.gameEngine.world.stepCount, objectInstance: obj @@ -220,7 +219,6 @@ class ServerEngine { // handle the object creation onObjectDestroyed(obj) { - console.log('object destroyed event'); this.networkTransmitter.addNetworkedEvent('objectDestroy', { stepCount: this.gameEngine.world.stepCount, objectInstance: obj @@ -349,5 +347,3 @@ class ServerEngine { } } - -module.exports = ServerEngine; diff --git a/src/Synchronizer.js b/src/Synchronizer.js index e777d32..0d41ef9 100644 --- a/src/Synchronizer.js +++ b/src/Synchronizer.js @@ -1,16 +1,14 @@ -"use strict"; +import InterpolateStrategy from './syncStrategies/InterpolateStrategy'; +import ExtrapolateStrategy from './syncStrategies/ExtrapolateStrategy'; +import FrameSyncStrategy from './syncStrategies/FrameSyncStrategy'; -const InterpolateStrategy = require('./syncStrategies/InterpolateStrategy'); -const ExtrapolateStrategy = require('./syncStrategies/ExtrapolateStrategy'); -const FrameSyncStrategy = require('./syncStrategies/FrameSyncStrategy'); const strategies = { extrapolate: ExtrapolateStrategy, interpolate: InterpolateStrategy, frameSync: FrameSyncStrategy }; -class Synchronizer { - +export default class Synchronizer { // create a synchronizer instance constructor(clientEngine, options) { this.clientEngine = clientEngine; @@ -20,6 +18,4 @@ class Synchronizer { } this.syncStrategy = new strategies[this.options.sync](this.clientEngine, this.options); } -} - -module.exports = Synchronizer; +} \ No newline at end of file diff --git a/src/controls/KeyboardControls.js b/src/controls/KeyboardControls.js index a8e21b0..3176efe 100644 --- a/src/controls/KeyboardControls.js +++ b/src/controls/KeyboardControls.js @@ -1,24 +1,178 @@ -const EventEmitter = require('eventemitter3'); +import EventEmitter from 'eventemitter3'; -//todo add all keyboard keys +// based on http://keycode.info/ // keyboard handling const keyCodeTable = { - 32: 'space', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 65: 'a', - 87: 'w', - 68: 'd', - 83: 's' + 3 : "break", + 8 : "backspace", //backspace / delete + 9 : "tab", + 12 : 'clear', + 13 : "enter", + 16 : "shift", + 17 : "ctrl", + 18 : "alt", + 19 : "pause/break", + 20 : "caps lock", + 27 : "escape", + 28 : "conversion", + 29 : "non-conversion", + 32 : "space", + 33 : "page up", + 34 : "page down", + 35 : "end", + 36 : "home", + 37 : "left", + 38 : "up", + 39 : "right", + 40 : "down", + 41 : "select", + 42 : "print", + 43 : "execute", + 44 : "Print Screen", + 45 : "insert", + 46 : "delete", + 48 : "0", + 49 : "1", + 50 : "2", + 51 : "3", + 52 : "4", + 53 : "5", + 54 : "6", + 55 : "7", + 56 : "8", + 57 : "9", + 58 : ":", + 59 : "semicolon (firefox), equals", + 60 : "<", + 61 : "equals (firefox)", + 63 : "ß", + 64 : "@", + 65 : "a", + 66 : "b", + 67 : "c", + 68 : "d", + 69 : "e", + 70 : "f", + 71 : "g", + 72 : "h", + 73 : "i", + 74 : "j", + 75 : "k", + 76 : "l", + 77 : "m", + 78 : "n", + 79 : "o", + 80 : "p", + 81 : "q", + 82 : "r", + 83 : "s", + 84 : "t", + 85 : "u", + 86 : "v", + 87 : "w", + 88 : "x", + 89 : "y", + 90 : "z", + 91 : "Windows Key / Left ⌘ / Chromebook Search key", + 92 : "right window key", + 93 : "Windows Menu / Right ⌘", + 96 : "numpad 0", + 97 : "numpad 1", + 98 : "numpad 2", + 99 : "numpad 3", + 100 : "numpad 4", + 101 : "numpad 5", + 102 : "numpad 6", + 103 : "numpad 7", + 104 : "numpad 8", + 105 : "numpad 9", + 106 : "multiply", + 107 : "add", + 108 : "numpad period (firefox)", + 109 : "subtract", + 110 : "decimal point", + 111 : "divide", + 112 : "f1", + 113 : "f2", + 114 : "f3", + 115 : "f4", + 116 : "f5", + 117 : "f6", + 118 : "f7", + 119 : "f8", + 120 : "f9", + 121 : "f10", + 122 : "f11", + 123 : "f12", + 124 : "f13", + 125 : "f14", + 126 : "f15", + 127 : "f16", + 128 : "f17", + 129 : "f18", + 130 : "f19", + 131 : "f20", + 132 : "f21", + 133 : "f22", + 134 : "f23", + 135 : "f24", + 144 : "num lock", + 145 : "scroll lock", + 160 : "^", + 161: '!', + 163 : "#", + 164: '$', + 165: 'ù', + 166 : "page backward", + 167 : "page forward", + 169 : "closing paren (AZERTY)", + 170: '*', + 171 : "~ + * key", + 173 : "minus (firefox), mute/unmute", + 174 : "decrease volume level", + 175 : "increase volume level", + 176 : "next", + 177 : "previous", + 178 : "stop", + 179 : "play/pause", + 180 : "e-mail", + 181 : "mute/unmute (firefox)", + 182 : "decrease volume level (firefox)", + 183 : "increase volume level (firefox)", + 186 : "semi-colon / ñ", + 187 : "equal sign", + 188 : "comma", + 189 : "dash", + 190 : "period", + 191 : "forward slash / ç", + 192 : "grave accent / ñ / æ", + 193 : "?, / or °", + 194 : "numpad period (chrome)", + 219 : "open bracket", + 220 : "back slash", + 221 : "close bracket / å", + 222 : "single quote / ø", + 223 : "`", + 224 : "left or right ⌘ key (firefox)", + 225 : "altgr", + 226 : "< /git >", + 230 : "GNOME Compose Key", + 231 : "ç", + 233 : "XF86Forward", + 234 : "XF86Back", + 240 : "alphanumeric", + 242 : "hiragana/katakana", + 243 : "half-width/full-width", + 244 : "kanji", + 255 : "toggle touchpad" }; + /** * This class allows easy usage of device keyboard controls */ -class KeyboardControls{ +export default class KeyboardControls{ constructor(clientEngine){ Object.assign(this, EventEmitter.prototype); @@ -71,7 +225,7 @@ class KeyboardControls{ e = e || window.event; let keyName = keyCodeTable[e.keyCode]; - if (keyName) { + if (keyName && this.boundKeys[keyName]) { if (this.keyState[keyName] == null){ this.keyState[keyName] = { count: 0 @@ -88,6 +242,4 @@ class KeyboardControls{ e.preventDefault(); } } -} - -module.exports = KeyboardControls; \ No newline at end of file +} \ No newline at end of file diff --git a/src/game/Timer.js b/src/game/Timer.js new file mode 100644 index 0000000..838e04e --- /dev/null +++ b/src/game/Timer.js @@ -0,0 +1,101 @@ +export default class Timer { + + constructor() { + this.currentTime = 0; + this.isActive = false; + this.idCounter = 0; + + this.events = {}; + } + + play() { + this.isActive = true; + } + + tick() { + let event; + let eventId; + + if (this.isActive) { + this.currentTime++; + + for (eventId in this.events) { + event = this.events[eventId]; + if (event) { + + if (event.type == 'repeat') { + if ((this.currentTime - event.startOffset) % event.time == 0) { + event.callback.apply(event.thisContext, event.args); + } + } + if (event.type == 'single') { + if ((this.currentTime - event.startOffset) % event.time == 0) { + event.callback.apply(event.thisContext, event.args); + event.destroy(); + } + } + + } + + } + } + } + + destroyEvent(eventId) { + delete this.events[eventId]; + } + + loop(time, callback) { + let timerEvent = new TimerEvent(this, + TimerEvent.TYPES.repeat, + time, + callback + ); + + this.events[timerEvent.id] = timerEvent; + + return timerEvent; + } + + add(time, callback, thisContext, args) { + let timerEvent = new TimerEvent(this, + TimerEvent.TYPES.single, + time, + callback, + thisContext, + args + ); + + this.events[timerEvent.id] = timerEvent; + return timerEvent; + } + + // todo implement timer delete all events + + destroy(id) { + delete this.events[id]; + } +} + +// timer event +class TimerEvent { + constructor(timer, type, time, callback, thisContext, args) { + this.id = ++timer.idCounter; + this.timer = timer; + this.type = type; + this.time = time; + this.callback = callback; + this.startOffset = timer.currentTime; + this.thisContext = thisContext; + this.args = args; + + this.destroy = function() { + this.timer.destroy(this.id); + }; + } +} + +TimerEvent.TYPES = { + repeat: 'repeat', + single: 'single' +}; \ No newline at end of file diff --git a/src/lib/MathUtils.js b/src/lib/MathUtils.js index de32258..d2c9ea6 100644 --- a/src/lib/MathUtils.js +++ b/src/lib/MathUtils.js @@ -1,6 +1,4 @@ -"use strict"; - -class MathUtils { +export default class MathUtils { // interpolate from start to end, advancing "percent" of the way static interpolate(start, end, percent) { @@ -35,6 +33,4 @@ class MathUtils { if (interpolatedVal < 0) interpolatedVal += wrapLength; return interpolatedVal; } -} - -module.exports = MathUtils; +} \ No newline at end of file diff --git a/src/lib/Scheduler.js b/src/lib/Scheduler.js index 9c5e797..5aca6d1 100644 --- a/src/lib/Scheduler.js +++ b/src/lib/Scheduler.js @@ -1,5 +1,4 @@ -'use strict'; -const EventEmitter = require('eventemitter3'); +import EventEmitter from 'eventemitter3'; const SIXTY_PER_SEC = 1000 / 60; const LOOP_SLOW_THRESH = 0.3; @@ -9,7 +8,7 @@ const LOOP_SLOW_COUNT = 10; * Scheduler class * */ -class Scheduler { +export default class Scheduler { /** * schedule a function to be called @@ -95,6 +94,4 @@ class Scheduler { hurryTick() { this.requestedDelay -= this.options.delay; } -} - -module.exports = Scheduler; +} \ No newline at end of file diff --git a/src/lib/Trace.js b/src/lib/Trace.js index ac25da0..919463e 100644 --- a/src/lib/Trace.js +++ b/src/lib/Trace.js @@ -1,51 +1,83 @@ -'use strict'; +/** + * Tracing Services. + * Use the trace functions to trace game state. Turn on tracing by + * specifying the minimum trace level which should be recorded. For + * example, setting traceLevel to Trace.TRACE_INFO will cause info, + * warn, and error traces to be recorded. + */ + export default class Trace { -class Trace { + constructor(options) { - constructor(options) { + this.options = Object.assign({ + traceLevel: this.TRACE_DEBUG + }, options); - this.options = Object.assign({ - traceLevel: this.TRACE_DEBUG - }, options); - - this.traceBuffer = []; - this.step = 'initializing'; + this.traceBuffer = []; + this.step = 'initializing'; // syntactic sugar functions - this.error = this.trace.bind(this, Trace.TRACE_ERROR); - this.warn = this.trace.bind(this, Trace.TRACE_WARN); - this.info = this.trace.bind(this, Trace.TRACE_INFO); - this.debug = this.trace.bind(this, Trace.TRACE_DEBUG); - this.trace = this.trace.bind(this, Trace.TRACE_ALL); - } - - static get TRACE_ALL() { return 0; } - static get TRACE_DEBUG() { return 1; } - static get TRACE_INFO() { return 2; } - static get TRACE_WARN() { return 3; } - static get TRACE_ERROR() { return 4; } - static get TRACE_NONE() { return 1000; } - - trace(level, data) { - if (level < this.options.traceLevel) - return; - - this.traceBuffer.push({ data, level, step: this.step, time: new Date() }); - } - - rotate() { - let buffer = this.traceBuffer; - this.traceBuffer = []; - return buffer; - } - - get length() { - return this.traceBuffer.length; - } - - setStep(s) { - this.step = s; - } -} + this.error = this.trace.bind(this, Trace.TRACE_ERROR); + this.warn = this.trace.bind(this, Trace.TRACE_WARN); + this.info = this.trace.bind(this, Trace.TRACE_INFO); + this.debug = this.trace.bind(this, Trace.TRACE_DEBUG); + this.trace = this.trace.bind(this, Trace.TRACE_ALL); + } + + /** + * Include all trace levels. + * @member {Number} TRACE_ALL + */ + static get TRACE_ALL() { return 0; } + + /** + * Include debug traces and higher. + * @member {Number} TRACE_DEBUG + */ + static get TRACE_DEBUG() { return 1; } + + /** + * Include info traces and higher. + * @member {Number} TRACE_INFO + */ + static get TRACE_INFO() { return 2; } + + /** + * Include warn traces and higher. + * @member {Number} TRACE_WARN + */ + static get TRACE_WARN() { return 3; } + + /** + * Include error traces and higher. + * @member {Number} TRACE_ERROR + */ + static get TRACE_ERROR() { return 4; } -module.exports = Trace; + /** + * Disable all tracing. + * @member {Number} TRACE_NONE + */ + static get TRACE_NONE() { return 1000; } + + trace(level, dataCB) { + if (level < this.options.traceLevel) + return; + + this.traceBuffer.push({ data: dataCB(), level, step: this.step, time: new Date() }); + } + + rotate() { + let buffer = this.traceBuffer; + this.traceBuffer = []; + return buffer; + } + + get length() { + return this.traceBuffer.length; + } + + setStep(s) { + this.step = s; + } +} diff --git a/src/lib/Utils.js b/src/lib/Utils.js index fed94f6..9ee2723 100644 --- a/src/lib/Utils.js +++ b/src/lib/Utils.js @@ -1,11 +1,4 @@ -'use strict'; - -const crypto = require('crypto'); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; -function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - -class Utils { +export default class Utils { static hashStr(str, bits) { let hash = 5381; @@ -34,73 +27,6 @@ class Utils { return true; } - // return an array of objects according to key, value, or key and value matching - // from http://techslides.com/how-to-parse-and-search-json-in-javascript - static findInObjectByKeyVal(obj, key, val) { - var objects = []; - for (var i in obj) { - if (!obj.hasOwnProperty(i)) continue; - if (typeof obj[i] === 'object') { - objects = objects.concat(Utils.findInObjectByKeyVal(obj[i], key, val)); - } else - // if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) - if (i == key && obj[i] == val || i == key && val == '') { - objects.push(obj); - } else if (obj[i] == val && key == '') { - //only add if the object is not already in the array - if (objects.lastIndexOf(obj) == -1) { - objects.push(obj); - } - } - } - return objects; - } - - /** - * Sorts object fields - * @param {Object} obj Initial object - * @return {*} sorted object - */ - static sortedObjectString(obj) { - if ((typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) == 'object') { - var _ret = function () { - if (Array.isArray(obj)) { - return { - v: '[' + obj.sort().toString() + ']' - }; - } - var sortedObj = new Map(); - var keys = Object.keys(obj).sort(); - keys.forEach(function (key) { - sortedObj.set(key, Utils.sortedObjectString(obj[key])); - }); - return { - v: '{' + [].concat(_toConsumableArray(sortedObj)).toString() + '}' - }; - }(); - - if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; - } - return obj; - } - - /** - * Calculates object hash - * @param {Object} obj Object to hash - * @param {string} alg Crypto algorithm to use (default="sha256"); - * @param {string=} enc Hash encoding (default="hex") - * @return {string} Hash string - */ - static hash(obj, alg, enc) { - alg = alg ? alg : 'sha256'; - enc = enc ? enc : 'hex'; - - const sorted = Utils.sortedObjectString(obj); - return crypto.createHash(alg) - .update(sorted) - .digest(enc); - } - static httpGetPromise(url) { return new Promise((resolve, reject) => { let req = new XMLHttpRequest(); @@ -113,6 +39,4 @@ class Utils { req.send(); }); } -} - -module.exports = Utils; +} \ No newline at end of file diff --git a/src/network/NetworkMonitor.js b/src/network/NetworkMonitor.js index 9ef36ef..ba2cab1 100644 --- a/src/network/NetworkMonitor.js +++ b/src/network/NetworkMonitor.js @@ -1,12 +1,10 @@ -"use strict"; - -const EventEmitter = require('eventemitter3'); +import EventEmitter from 'eventemitter3'; /** * Measures network performance between the client and the server * Represents both the client and server portions of NetworkMonitor */ -class NetworkMonitor { +export default class NetworkMonitor { constructor() { // mixin for EventEmitter @@ -56,6 +54,4 @@ class NetworkMonitor { socket.emit("RTTResponse", queryId); } -} - -module.exports = NetworkMonitor; +} \ No newline at end of file diff --git a/src/network/NetworkTransmitter.js b/src/network/NetworkTransmitter.js index be861f3..729237a 100644 --- a/src/network/NetworkTransmitter.js +++ b/src/network/NetworkTransmitter.js @@ -1,20 +1,16 @@ -'use strict'; +import Serializer from './../serialize/Serializer'; -const Serializer = require('./../serialize/Serializer'); +import NetworkedEventFactory from './NetworkedEventFactory'; +import NetworkedEventCollection from './NetworkedEventCollection'; +import Utils from './../lib/Utils'; -const NetworkedEventFactory = require('./NetworkedEventFactory'); -const NetworkedEventCollection = require('./NetworkedEventCollection'); -const Utils = require('./../lib/Utils'); - -class NetworkTransmitter { +export default class NetworkTransmitter { constructor(serializer) { this.serializer = serializer; this.registeredEvents = []; - this.payload = []; - this.serializer.registerClass(NetworkedEventCollection); this.registerNetworkedEventFactory('objectUpdate', { @@ -44,6 +40,8 @@ class NetworkTransmitter { fullUpdate: { type: Serializer.TYPES.UINT8 } } }); + + this.networkedEventCollection = new NetworkedEventCollection(); } registerNetworkedEventFactory(eventName, options) { @@ -68,17 +66,16 @@ class NetworkTransmitter { } let stagedNetworkedEvent = this.registeredEvents[eventName].create(payload); - this.payload.push(stagedNetworkedEvent); + this.networkedEventCollection.events.push(stagedNetworkedEvent); return stagedNetworkedEvent; } serializePayload() { - if (this.payload.length === 0) + if (this.networkedEventCollection.events.length === 0) return null; - let networkedEventCollection = new NetworkedEventCollection(this.payload); - let dataBuffer = networkedEventCollection.serialize(this.serializer); + let dataBuffer = this.networkedEventCollection.serialize(this.serializer); return dataBuffer; } @@ -88,9 +85,7 @@ class NetworkTransmitter { } clearPayload() { - this.payload = []; + this.networkedEventCollection.events = []; } -} - -module.exports = NetworkTransmitter; +} \ No newline at end of file diff --git a/src/network/NetworkedEventCollection.js b/src/network/NetworkedEventCollection.js index 9660b99..4236047 100644 --- a/src/network/NetworkedEventCollection.js +++ b/src/network/NetworkedEventCollection.js @@ -1,12 +1,10 @@ -'use strict'; - -const Serializer = require('./../serialize/Serializer'); -const Serializable = require('./../serialize/Serializable'); +import Serializer from './../serialize/Serializer'; +import Serializable from './../serialize/Serializable'; /** * Defines a collection of NetworkEvents to be transmitted over the wire */ -class NetworkedEventCollection extends Serializable { +export default class NetworkedEventCollection extends Serializable { static get netScheme() { return { @@ -19,9 +17,7 @@ class NetworkedEventCollection extends Serializable { constructor(events) { super(); - this.events = events; + this.events = events || []; } -} - -module.exports = NetworkedEventCollection; +} \ No newline at end of file diff --git a/src/network/NetworkedEventFactory.js b/src/network/NetworkedEventFactory.js index 4dec82e..8e3f67c 100644 --- a/src/network/NetworkedEventFactory.js +++ b/src/network/NetworkedEventFactory.js @@ -1,9 +1,7 @@ -'use strict'; +import Serializable from './../serialize/Serializable'; +import Utils from './../lib/Utils'; -const Serializable = require('./../serialize/Serializable'); -const Utils = require('./../lib/Utils'); - -class NetworkedEventFactory { +export default class NetworkedEventFactory { constructor(serializer, eventName, options) { options = Object.assign({}, options); @@ -41,5 +39,3 @@ class NetworkedEventFactory { } } - -module.exports = NetworkedEventFactory; diff --git a/src/physics/CannonPhysicsEngine.js b/src/physics/CannonPhysicsEngine.js index ed71270..e79f6a3 100644 --- a/src/physics/CannonPhysicsEngine.js +++ b/src/physics/CannonPhysicsEngine.js @@ -1,14 +1,13 @@ -'use strict'; -const PhysicsEngine = require('./PhysicsEngine'); -const CANNON = require('cannon'); +import PhysicsEngine from './PhysicsEngine'; +import CANNON from 'cannon'; /** * CannonPhysicsEngine is a three-dimensional lightweight physics engine */ -class CannonPhysicsEngine extends PhysicsEngine { +export default class CannonPhysicsEngine extends PhysicsEngine { - init(options) { - super.init(options); + constructor(options) { + super(options); this.options.dt = this.options.dt || (1 / 60); let world = this.world = new CANNON.World(); @@ -55,5 +54,3 @@ class CannonPhysicsEngine extends PhysicsEngine { this.world.removeBody(obj); } } - -module.exports = CannonPhysicsEngine; diff --git a/src/physics/PhysicsEngine.js b/src/physics/PhysicsEngine.js index 9fd4af6..b0830a7 100644 --- a/src/physics/PhysicsEngine.js +++ b/src/physics/PhysicsEngine.js @@ -1,12 +1,14 @@ -'use strict'; - // The base Physics Engine class defines the expected interface // for all physics engines -class PhysicsEngine { +export default class PhysicsEngine { - init(options) { + constructor(options) { this.options = options; this.gameEngine = options.gameEngine; + + if (!options.gameEngine) { + console.warn('Physics engine initialized without gameEngine!'); + } } /** @@ -18,5 +20,3 @@ class PhysicsEngine { step(dt, objectFilter) {} } - -module.exports = PhysicsEngine; diff --git a/src/physics/PhysijsPhysicsEngine.js b/src/physics/PhysijsPhysicsEngine.js deleted file mode 100644 index 806c3a8..0000000 --- a/src/physics/PhysijsPhysicsEngine.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; - -const NodePhysijs = require('nodejs-physijs'); -const PhysicsEngine = require('./PhysicsEngine'); -const THREE = NodePhysijs.THREE; -const Ammo = NodePhysijs.Ammo; -let Physijs; - -class PhysijsPhysicsEngine extends PhysicsEngine { - - constructor() { - super(); - Physijs = NodePhysijs.Physijs(THREE, Ammo); // eslint-disable-line - this.scene = null; - } - - init() { - // TODO: now that we split physics/render is there still a need to - // store THREE and Physijs in "this"? - this.scene = new Physijs.Scene(); - this.THREE = THREE; - this.Physijs = Physijs; - } - - step() { - this.scene.simulate(); - } - -} - -module.exports = PhysijsPhysicsEngine; diff --git a/src/physics/SimplePhysics/CollisionDetection.js b/src/physics/SimplePhysics/BruteCollisionDetection.js similarity index 72% rename from src/physics/SimplePhysics/CollisionDetection.js rename to src/physics/SimplePhysics/BruteCollisionDetection.js index 41bae1e..a1dc450 100644 --- a/src/physics/SimplePhysics/CollisionDetection.js +++ b/src/physics/SimplePhysics/BruteCollisionDetection.js @@ -1,11 +1,8 @@ -'use strict'; - -const TwoVector = require('../../serialize/TwoVector'); - +import TwoVector from '../../serialize/TwoVector'; let differenceVector = new TwoVector(); // The collision detection of SimplePhysicsEngine is a brute-force approach -class CollisionDetection { +export default class CollisionDetection { constructor(options) { this.options = Object.assign({ COLLISION_DISTANCE: 28 }, options); @@ -41,11 +38,17 @@ class CollisionDetection { // detect by checking all pairs detect() { let objects = this.gameEngine.world.objects; - for (let k1 of Object.keys(objects)) - for (let k2 of Object.keys(objects)) + let keys = Object.keys(objects); + + //Delete non existed object's pairs + for (let pairId in this.collisionPairs) + if (this.collisionPairs.hasOwnProperty(pairId)) + if (keys.indexOf(pairId.split(',')[0]) === -1 || keys.indexOf(pairId.split(',')[1]) === -1) + delete this.collisionPairs[pairId]; + + for (let k1 of keys) + for (let k2 of keys) if (k2 > k1) this.checkPair(k1, k2); } -} - -module.exports = CollisionDetection; +} \ No newline at end of file diff --git a/src/physics/SimplePhysics/HSHG.js b/src/physics/SimplePhysics/HSHG.js new file mode 100644 index 0000000..f0b0ab5 --- /dev/null +++ b/src/physics/SimplePhysics/HSHG.js @@ -0,0 +1,578 @@ +// Hierarchical Spatial Hash Grid: HSHG +// source: https://gist.github.com/kirbysayshi/1760774 + +// --------------------------------------------------------------------- +// GLOBAL FUNCTIONS +// --------------------------------------------------------------------- + +/** + * Updates every object's position in the grid, but only if + * the hash value for that object has changed. + * This method DOES NOT take into account object expansion or + * contraction, just position, and does not attempt to change + * the grid the object is currently in; it only (possibly) changes + * the cell. + * + * If the object has significantly changed in size, the best bet is to + * call removeObject() and addObject() sequentially, outside of the + * normal update cycle of HSHG. + * + * @return void desc + */ +function update_RECOMPUTE() { + + var i, + obj, + grid, + meta, + objAABB, + newObjHash; + + // for each object + for (i = 0; i < this._globalObjects.length; i++) { + obj = this._globalObjects[i]; + meta = obj.HSHG; + grid = meta.grid; + + // recompute hash + objAABB = obj.getAABB(); + newObjHash = grid.toHash(objAABB.min[0], objAABB.min[1]); + + if (newObjHash !== meta.hash) { + // grid position has changed, update! + grid.removeObject(obj); + grid.addObject(obj, newObjHash); + } + } +} + +// not implemented yet :) +function update_REMOVEALL() { + +} + +function testAABBOverlap(objA, objB) { + var a = objA.getAABB(), + b = objB.getAABB(); + + // if(a.min[0] > b.max[0] || a.min[1] > b.max[1] || a.min[2] > b.max[2] + // || a.max[0] < b.min[0] || a.max[1] < b.min[1] || a.max[2] < b.min[2]){ + + if (a.min[0] > b.max[0] || a.min[1] > b.max[1] || + a.max[0] < b.min[0] || a.max[1] < b.min[1]) { + return false; + } + return true; + +} + +function getLongestAABBEdge(min, max) { + return Math.max( + Math.abs(max[0] - min[0]) + , Math.abs(max[1] - min[1]) + // ,Math.abs(max[2] - min[2]) + ); +} + +// --------------------------------------------------------------------- +// ENTITIES +// --------------------------------------------------------------------- + +function HSHG() { + + this.MAX_OBJECT_CELL_DENSITY = 1 / 8; // objects / cells + this.INITIAL_GRID_LENGTH = 256; // 16x16 + this.HIERARCHY_FACTOR = 2; + this.HIERARCHY_FACTOR_SQRT = Math.SQRT2; + this.UPDATE_METHOD = update_RECOMPUTE; // or update_REMOVEALL + + this._grids = []; + this._globalObjects = []; +} + +// HSHG.prototype.init = function(){ +// this._grids = []; +// this._globalObjects = []; +// } + +HSHG.prototype.addObject = function (obj) { + var x, i, + cellSize, + objAABB = obj.getAABB(), + objSize = getLongestAABBEdge(objAABB.min, objAABB.max), + oneGrid, newGrid; + + // for HSHG metadata + obj.HSHG = { + globalObjectsIndex: this._globalObjects.length + }; + + // add to global object array + this._globalObjects.push(obj); + + if (this._grids.length == 0) { + // no grids exist yet + cellSize = objSize * this.HIERARCHY_FACTOR_SQRT; + newGrid = new Grid(cellSize, this.INITIAL_GRID_LENGTH, this); + newGrid.initCells(); + newGrid.addObject(obj); + + this._grids.push(newGrid); + } else { + x = 0; + + // grids are sorted by cellSize, smallest to largest + for (i = 0; i < this._grids.length; i++) { + oneGrid = this._grids[i]; + x = oneGrid.cellSize; + if (objSize < x) { + x /= this.HIERARCHY_FACTOR; + if (objSize < x) { + // find appropriate size + while (objSize < x) { + x /= this.HIERARCHY_FACTOR; + } + newGrid = new Grid(x * this.HIERARCHY_FACTOR, this.INITIAL_GRID_LENGTH, this); + newGrid.initCells(); + // assign obj to grid + newGrid.addObject(obj); + // insert grid into list of grids directly before oneGrid + this._grids.splice(i, 0, newGrid); + } else { + // insert obj into grid oneGrid + oneGrid.addObject(obj); + } + return; + } + } + + while (objSize >= x) { + x *= this.HIERARCHY_FACTOR; + } + + newGrid = new Grid(x, this.INITIAL_GRID_LENGTH, this); + newGrid.initCells(); + // insert obj into grid + newGrid.addObject(obj); + // add newGrid as last element in grid list + this._grids.push(newGrid); + } +}; + +HSHG.prototype.removeObject = function (obj) { + var meta = obj.HSHG, + globalObjectsIndex, + replacementObj; + + if (meta === undefined) { + throw Error(obj + ' was not in the HSHG.'); + return; + } + + // remove object from global object list + globalObjectsIndex = meta.globalObjectsIndex; + if (globalObjectsIndex === this._globalObjects.length - 1) { + this._globalObjects.pop(); + } else { + replacementObj = this._globalObjects.pop(); + replacementObj.HSHG.globalObjectsIndex = globalObjectsIndex; + this._globalObjects[globalObjectsIndex] = replacementObj; + } + + meta.grid.removeObject(obj); + + // remove meta data + delete obj.HSHG; +}; + +HSHG.prototype.update = function () { + this.UPDATE_METHOD.call(this); +}; + +HSHG.prototype.queryForCollisionPairs = function (broadOverlapTestCallback) { + + var i, j, k, l, c, + grid, + cell, + objA, + objB, + offset, + adjacentCell, + biggerGrid, + objAAABB, + objAHashInBiggerGrid, + possibleCollisions = []; + + // default broad test to internal aabb overlap test + let broadOverlapTest = broadOverlapTestCallback || testAABBOverlap; + + // for all grids ordered by cell size ASC + for (i = 0; i < this._grids.length; i++) { + grid = this._grids[i]; + + // for each cell of the grid that is occupied + for (j = 0; j < grid.occupiedCells.length; j++) { + cell = grid.occupiedCells[j]; + + // collide all objects within the occupied cell + for (k = 0; k < cell.objectContainer.length; k++) { + objA = cell.objectContainer[k]; + for (l = k + 1; l < cell.objectContainer.length; l++) { + objB = cell.objectContainer[l]; + if (broadOverlapTest(objA, objB) === true) { + possibleCollisions.push([objA, objB]); + } + } + } + + // for the first half of all adjacent cells (offset 4 is the current cell) + for (c = 0; c < 4; c++) { + offset = cell.neighborOffsetArray[c]; + + // if(offset === null) { continue; } + + adjacentCell = grid.allCells[cell.allCellsIndex + offset]; + + // collide all objects in cell with adjacent cell + for (k = 0; k < cell.objectContainer.length; k++) { + objA = cell.objectContainer[k]; + for (l = 0; l < adjacentCell.objectContainer.length; l++) { + objB = adjacentCell.objectContainer[l]; + if (broadOverlapTest(objA, objB) === true) { + possibleCollisions.push([objA, objB]); + } + } + } + } + } + + // forall objects that are stored in this grid + for (j = 0; j < grid.allObjects.length; j++) { + objA = grid.allObjects[j]; + objAAABB = objA.getAABB(); + + // for all grids with cellsize larger than grid + for (k = i + 1; k < this._grids.length; k++) { + biggerGrid = this._grids[k]; + objAHashInBiggerGrid = biggerGrid.toHash(objAAABB.min[0], objAAABB.min[1]); + cell = biggerGrid.allCells[objAHashInBiggerGrid]; + + // check objA against every object in all cells in offset array of cell + // for all adjacent cells... + for (c = 0; c < cell.neighborOffsetArray.length; c++) { + offset = cell.neighborOffsetArray[c]; + + // if(offset === null) { continue; } + + adjacentCell = biggerGrid.allCells[cell.allCellsIndex + offset]; + + // for all objects in the adjacent cell... + for (l = 0; l < adjacentCell.objectContainer.length; l++) { + objB = adjacentCell.objectContainer[l]; + // test against object A + if (broadOverlapTest(objA, objB) === true) { + possibleCollisions.push([objA, objB]); + } + } + } + } + } + } + + // return list of object pairs + return possibleCollisions; +}; + +HSHG.update_RECOMPUTE = update_RECOMPUTE; +HSHG.update_REMOVEALL = update_REMOVEALL; + +/** + * Grid + * + * @constructor + * @param int cellSize the pixel size of each cell of the grid + * @param int cellCount the total number of cells for the grid (width x height) + * @param HSHG parentHierarchy the HSHG to which this grid belongs + * @return void + */ +function Grid(cellSize, cellCount, parentHierarchy) { + this.cellSize = cellSize; + this.inverseCellSize = 1 / cellSize; + this.rowColumnCount = ~~Math.sqrt(cellCount); + this.xyHashMask = this.rowColumnCount - 1; + this.occupiedCells = []; + this.allCells = Array(this.rowColumnCount * this.rowColumnCount); + this.allObjects = []; + this.sharedInnerOffsets = []; + + this._parentHierarchy = parentHierarchy || null; +} + +Grid.prototype.initCells = function () { + + // TODO: inner/unique offset rows 0 and 2 may need to be + // swapped due to +y being "down" vs "up" + + var i, + gridLength = this.allCells.length, + x, y, + wh = this.rowColumnCount, + isOnRightEdge, isOnLeftEdge, isOnTopEdge, isOnBottomEdge, + innerOffsets = [ + // y+ down offsets + // -1 + -wh, -wh, -wh + 1, + // -1, 0, 1, + // wh - 1, wh, wh + 1 + + // y+ up offsets + wh - 1, wh, wh + 1, + -1, 0, 1, + -1 + -wh, -wh, -wh + 1 + ], + leftOffset, rightOffset, topOffset, bottomOffset, + uniqueOffsets = [], + cell; + + this.sharedInnerOffsets = innerOffsets; + + // init all cells, creating offset arrays as needed + + for (i = 0; i < gridLength; i++) { + + cell = new Cell(); + // compute row (y) and column (x) for an index + y = ~~(i / this.rowColumnCount); + x = ~~(i - (y * this.rowColumnCount)); + + // reset / init + isOnRightEdge = false; + isOnLeftEdge = false; + isOnTopEdge = false; + isOnBottomEdge = false; + + // right or left edge cell + if ((x + 1) % this.rowColumnCount == 0) { + isOnRightEdge = true; + } else if (x % this.rowColumnCount == 0) { + isOnLeftEdge = true; + } + + // top or bottom edge cell + if ((y + 1) % this.rowColumnCount == 0) { + isOnTopEdge = true; + } else if (y % this.rowColumnCount == 0) { + isOnBottomEdge = true; + } + + // if cell is edge cell, use unique offsets, otherwise use inner offsets + if (isOnRightEdge || isOnLeftEdge || isOnTopEdge || isOnBottomEdge) { + + // figure out cardinal offsets first + rightOffset = isOnRightEdge === true ? -wh + 1 : 1; + leftOffset = isOnLeftEdge === true ? wh - 1 : -1; + topOffset = isOnTopEdge === true ? -gridLength + wh : wh; + bottomOffset = isOnBottomEdge === true ? gridLength - wh : -wh; + + // diagonals are composites of the cardinals + uniqueOffsets = [ + // y+ down offset + // leftOffset + bottomOffset, bottomOffset, rightOffset + bottomOffset, + // leftOffset, 0, rightOffset, + // leftOffset + topOffset, topOffset, rightOffset + topOffset + + // y+ up offset + leftOffset + topOffset, topOffset, rightOffset + topOffset, + leftOffset, 0, rightOffset, + leftOffset + bottomOffset, bottomOffset, rightOffset + bottomOffset + ]; + + cell.neighborOffsetArray = uniqueOffsets; + } else { + cell.neighborOffsetArray = this.sharedInnerOffsets; + } + + cell.allCellsIndex = i; + this.allCells[i] = cell; + } +}; + +Grid.prototype.toHash = function (x, y, z) { + var i, xHash, yHash, zHash; + + if (x < 0) { + i = (-x) * this.inverseCellSize; + xHash = this.rowColumnCount - 1 - (~~i & this.xyHashMask); + } else { + i = x * this.inverseCellSize; + xHash = ~~i & this.xyHashMask; + } + + if (y < 0) { + i = (-y) * this.inverseCellSize; + yHash = this.rowColumnCount - 1 - (~~i & this.xyHashMask); + } else { + i = y * this.inverseCellSize; + yHash = ~~i & this.xyHashMask; + } + + // if(z < 0){ + // i = (-z) * this.inverseCellSize; + // zHash = this.rowColumnCount - 1 - ( ~~i & this.xyHashMask ); + // } else { + // i = z * this.inverseCellSize; + // zHash = ~~i & this.xyHashMask; + // } + + return xHash + yHash * this.rowColumnCount; + // + zHash * this.rowColumnCount * this.rowColumnCount; +}; + +Grid.prototype.addObject = function (obj, hash) { + var objAABB, + objHash, + targetCell; + + // technically, passing this in this should save some computational effort when updating objects + if (hash !== undefined) { + objHash = hash; + } else { + objAABB = obj.getAABB(); + objHash = this.toHash(objAABB.min[0], objAABB.min[1]); + } + targetCell = this.allCells[objHash]; + + if (targetCell.objectContainer.length === 0) { + // insert this cell into occupied cells list + targetCell.occupiedCellsIndex = this.occupiedCells.length; + this.occupiedCells.push(targetCell); + } + + // add meta data to obj, for fast update/removal + obj.HSHG.objectContainerIndex = targetCell.objectContainer.length; + obj.HSHG.hash = objHash; + obj.HSHG.grid = this; + obj.HSHG.allGridObjectsIndex = this.allObjects.length; + // add obj to cell + targetCell.objectContainer.push(obj); + + // we can assume that the targetCell is already a member of the occupied list + + // add to grid-global object list + this.allObjects.push(obj); + + // do test for grid density + if (this.allObjects.length / this.allCells.length > this._parentHierarchy.MAX_OBJECT_CELL_DENSITY) { + // grid must be increased in size + this.expandGrid(); + } +}; + +Grid.prototype.removeObject = function (obj) { + var meta = obj.HSHG, + hash, + containerIndex, + allGridObjectsIndex, + cell, + replacementCell, + replacementObj; + + hash = meta.hash; + containerIndex = meta.objectContainerIndex; + allGridObjectsIndex = meta.allGridObjectsIndex; + cell = this.allCells[hash]; + + // remove object from cell object container + if (cell.objectContainer.length === 1) { + // this is the last object in the cell, so reset it + cell.objectContainer.length = 0; + + // remove cell from occupied list + if (cell.occupiedCellsIndex === this.occupiedCells.length - 1) { + // special case if the cell is the newest in the list + this.occupiedCells.pop(); + } else { + replacementCell = this.occupiedCells.pop(); + replacementCell.occupiedCellsIndex = cell.occupiedCellsIndex; + this.occupiedCells[cell.occupiedCellsIndex] = replacementCell; + } + + cell.occupiedCellsIndex = null; + } else { + // there is more than one object in the container + if (containerIndex === cell.objectContainer.length - 1) { + // special case if the obj is the newest in the container + cell.objectContainer.pop(); + } else { + replacementObj = cell.objectContainer.pop(); + replacementObj.HSHG.objectContainerIndex = containerIndex; + cell.objectContainer[containerIndex] = replacementObj; + } + } + + // remove object from grid object list + if (allGridObjectsIndex === this.allObjects.length - 1) { + this.allObjects.pop(); + } else { + replacementObj = this.allObjects.pop(); + replacementObj.HSHG.allGridObjectsIndex = allGridObjectsIndex; + this.allObjects[allGridObjectsIndex] = replacementObj; + } +}; + +Grid.prototype.expandGrid = function () { + var i, j, + currentCellCount = this.allCells.length, + currentRowColumnCount = this.rowColumnCount, + currentXYHashMask = this.xyHashMask, + + newCellCount = currentCellCount * 4, // double each dimension + newRowColumnCount = ~~Math.sqrt(newCellCount), + newXYHashMask = newRowColumnCount - 1, + allObjects = this.allObjects.slice(0), // duplicate array, not objects contained + aCell, + push = Array.prototype.push; + + // remove all objects + for (i = 0; i < allObjects.length; i++) { + this.removeObject(allObjects[i]); + } + + // reset grid values, set new grid to be 4x larger than last + this.rowColumnCount = newRowColumnCount; + this.allCells = Array(this.rowColumnCount * this.rowColumnCount); + this.xyHashMask = newXYHashMask; + + // initialize new cells + this.initCells(); + + // re-add all objects to grid + for (i = 0; i < allObjects.length; i++) { + this.addObject(allObjects[i]); + } +}; + +/** + * A cell of the grid + * + * @constructor + * @return void desc + */ +function Cell() { + this.objectContainer = []; + this.neighborOffsetArray; + this.occupiedCellsIndex = null; + this.allCellsIndex = null; +} + +// --------------------------------------------------------------------- +// EXPORTS +// --------------------------------------------------------------------- + +HSHG._private = { + Grid: Grid, + Cell: Cell, + testAABBOverlap: testAABBOverlap, + getLongestAABBEdge: getLongestAABBEdge +}; + +export default HSHG; diff --git a/src/physics/SimplePhysics/HSHGCollisionDetection.js b/src/physics/SimplePhysics/HSHGCollisionDetection.js new file mode 100644 index 0000000..58c05fd --- /dev/null +++ b/src/physics/SimplePhysics/HSHGCollisionDetection.js @@ -0,0 +1,75 @@ +import HSHG from './HSHG'; + +// Collision detection based on Hierarchical Spatial Hash Grid +// uses this implementation https://gist.github.com/kirbysayshi/1760774 +export default class HSHGCollisionDetection { + + constructor(options) { + this.options = Object.assign({ COLLISION_DISTANCE: 28 }, options); + } + + init(options) { + this.gameEngine = options.gameEngine; + this.grid = new HSHG(); + this.previousCollisionPairs = {}; + this.stepCollidingPairs = {}; + + this.gameEngine.on('objectAdded', obj => { + // add the gameEngine obj the the spatial grid + this.grid.addObject(obj); + }); + + this.gameEngine.on('objectDestroyed', obj => { + // add the gameEngine obj the the spatial grid + this.grid.removeObject(obj); + }); + } + + detect() { + this.grid.update(); + this.stepCollidingPairs = this.grid.queryForCollisionPairs().reduce((accumulator, currentValue, i) => { + let pairId = getArrayPairId(currentValue); + accumulator[pairId] = { o1: currentValue[0], o2: currentValue[1] }; + return accumulator; + }, {}); + + for (let pairId of Object.keys(this.previousCollisionPairs)){ + let pairObj = this.previousCollisionPairs[pairId]; + + // existed in previous pairs, but not during this step: this pair stopped colliding + if (pairId in this.stepCollidingPairs === false) { + this.gameEngine.emit('collisionStop', pairObj); + } + } + + for (let pairId of Object.keys(this.stepCollidingPairs)) { + let pairObj = this.stepCollidingPairs[pairId]; + + // didn't exist in previous pairs, but exists now: this is a new colliding pair + if (pairId in this.previousCollisionPairs === false) { + this.gameEngine.emit('collisionStart', pairObj); + } + } + + this.previousCollisionPairs = this.stepCollidingPairs; + } + + /** + * checks wheter two objects are currently colliding + * @param o1 {Object} first object + * @param o2 {Object} second object + * @returns {boolean} are the two objects colliding? + */ + areObjectsColliding(o1, o2){ + return getArrayPairId([o1,o2]) in this.stepCollidingPairs; + } + +} + +function getArrayPairId(arrayPair){ + // make sure to get the same id regardless of object order + let sortedArrayPair = arrayPair.slice(0).sort(); + return sortedArrayPair[0].id + '-' + sortedArrayPair[1].id; +} + +module.exports = HSHGCollisionDetection ; diff --git a/src/physics/SimplePhysicsEngine.js b/src/physics/SimplePhysicsEngine.js index 56090d6..76bb59f 100644 --- a/src/physics/SimplePhysicsEngine.js +++ b/src/physics/SimplePhysicsEngine.js @@ -1,24 +1,31 @@ 'use strict'; -const PhysicsEngine = require('./PhysicsEngine'); -const CollisionDetection = require('./SimplePhysics/CollisionDetection'); -const TwoVector = require('../serialize/TwoVector'); +import PhysicsEngine from './PhysicsEngine'; +import TwoVector from '../serialize/TwoVector'; +import HSHGCollisionDetection from './SimplePhysics/HSHGCollisionDetection'; +import BruteCollisionDetection from './SimplePhysics/BruteCollisionDetection'; let dv = new TwoVector(); /** * SimplePhysicsEngine is a pseudo-physics engine which works with * objects of class DynamicObject. */ -class SimplePhysicsEngine extends PhysicsEngine { +export default class SimplePhysicsEngine extends PhysicsEngine { - init(initOptions) { - super.init(initOptions); - this.collisionDetection = new CollisionDetection(); + constructor(initOptions) { + super(initOptions); + + // todo does this mean both modules always get loaded? + if (initOptions.collisions && initOptions.collisions.type === 'HSHG') { + this.collisionDetection = new HSHGCollisionDetection(); + } else { + this.collisionDetection = new BruteCollisionDetection(); + } /** * The actor's name. * @member {TwoVector} constant gravity affecting all objects */ - this.gravity = new TwoVector(0,0); + this.gravity = new TwoVector(0, 0); if (initOptions.gravity) this.gravity.copy(initOptions.gravity); @@ -45,7 +52,7 @@ class SimplePhysicsEngine extends PhysicsEngine { o.velocity.add(dv); } - //apply gravity + // apply gravity if (o.affectedByGravity) o.velocity.add(this.gravity); let velMagnitude = o.velocity.length(); @@ -94,5 +101,3 @@ class SimplePhysicsEngine extends PhysicsEngine { this.collisionDetection.detect(this.gameEngine); } } - -module.exports = SimplePhysicsEngine; diff --git a/src/render/AFrameRenderer.js b/src/render/AFrameRenderer.js index c7dd7a9..22c4acd 100644 --- a/src/render/AFrameRenderer.js +++ b/src/render/AFrameRenderer.js @@ -1,13 +1,12 @@ -'use strict'; /* globals AFRAME */ -const Renderer = require('./Renderer'); -const networkedPhysics = require('./aframe/system'); +import Renderer from './Renderer'; +import networkedPhysics from './aframe/system'; /** * The A-Frame Renderer */ -class AFrameRenderer extends Renderer { +export default class AFrameRenderer extends Renderer { /** * Constructor of the Renderer singleton. @@ -57,6 +56,4 @@ class AFrameRenderer extends Renderer { super.draw(t, dt); } -} - -module.exports = AFrameRenderer; +} \ No newline at end of file diff --git a/src/render/Renderer.js b/src/render/Renderer.js index bfabd65..da16afb 100644 --- a/src/render/Renderer.js +++ b/src/render/Renderer.js @@ -1,6 +1,6 @@ -'use strict'; +import EventEmitter from 'eventemitter3'; -const EventEmitter = require('eventemitter3'); +let singleton = null; const TIME_RESET_THRESHOLD = 100; @@ -10,7 +10,11 @@ const TIME_RESET_THRESHOLD = 100; * method. The draw method will be invoked on every iteration of the browser's * render loop. */ -class Renderer { +export default class Renderer { + + static getInstance() { + return singleton; + } /** * Constructor of the Renderer singleton. @@ -26,6 +30,9 @@ class Renderer { // mixin for EventEmitter Object.assign(this, EventEmitter.prototype); + + // the singleton renderer has been created + singleton = this; } /** @@ -72,13 +79,13 @@ class Renderer { this.clientEngine.lastStepTime = t - p / 2; this.clientEngine.correction = p / 2; // HACK: remove next line - this.clientEngine.gameEngine.trace.trace(`============RESETTING lastTime=${this.clientEngine.lastStepTime} period=${p}`); + this.clientEngine.gameEngine.trace.trace(() => `============RESETTING lastTime=${this.clientEngine.lastStepTime} period=${p}`); } // catch-up missed steps while (t > this.clientEngine.lastStepTime + p) { // HACK: remove next line -this.clientEngine.gameEngine.trace.trace(`============RENDERER DRAWING EXTRA t=${t} LST=${this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); +this.clientEngine.gameEngine.trace.trace(() => `============RENDERER DRAWING EXTRA t=${t} LST=${this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); this.clientEngine.step(this.clientEngine.lastStepTime + p, p + this.clientEngine.correction); this.clientEngine.lastStepTime += p; this.clientEngine.correction = 0; @@ -88,7 +95,7 @@ this.clientEngine.gameEngine.trace.trace(`============RENDERER DRAWING EXTRA t=$ // might happen after catch up above if (t < this.clientEngine.lastStepTime) { // HACK: remove next line - this.clientEngine.gameEngine.trace.trace(`============RENDERER DRAWING NOSTEP t=${t} dt=${t - this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); + this.clientEngine.gameEngine.trace.trace(() => `============RENDERER DRAWING NOSTEP t=${t} dt=${t - this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); dt = t - this.clientEngine.lastStepTime + this.clientEngine.correction; if (dt < 0) dt = 0; @@ -100,7 +107,7 @@ this.clientEngine.gameEngine.trace.trace(`============RENDERER DRAWING EXTRA t=$ // render-controlled step // HACK: remove next line - this.clientEngine.gameEngine.trace.trace(`============RENDERER DRAWING t=${t} LST=${this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); + this.clientEngine.gameEngine.trace.trace(() => `============RENDERER DRAWING t=${t} LST=${this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); dt = t - this.clientEngine.lastStepTime + this.clientEngine.correction; this.clientEngine.lastStepTime += p; @@ -108,22 +115,18 @@ this.clientEngine.gameEngine.trace.trace(`============RENDERER DRAWING EXTRA t=$ this.clientEngine.step(t, dt); // HACK: remove next line -this.clientEngine.gameEngine.trace.trace(`============RENDERER DONE t=${t} LST=${this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); +this.clientEngine.gameEngine.trace.trace(() => `============RENDERER DONE t=${t} LST=${this.clientEngine.lastStepTime} correction = ${this.clientEngine.correction} period=${p}`); } /** * Handle the addition of a new object to the world. * @param {Object} obj - The object to be added. */ - addObject(obj) { - } + addObject(obj) {} /** * Handle the removal of an old object from the world. * @param {Object} obj - The object to be removed. */ - removeObject(obj) { - } + removeObject(obj) {} } - -module.exports = Renderer; diff --git a/src/render/ThreeRenderer.js b/src/render/ThreeRenderer.js index d5b9f5a..8bba302 100644 --- a/src/render/ThreeRenderer.js +++ b/src/render/ThreeRenderer.js @@ -1,7 +1,5 @@ /* global THREE */ -'use strict'; - -const Renderer = require('./Renderer'); +import Renderer from './Renderer'; // TODO: I have mixed feelings about this class. It doesn't actually provide // anything useful. I assume each game will write their own renderer even in THREE. @@ -9,7 +7,7 @@ const Renderer = require('./Renderer'); // But it hijacks the creation of the scene and the THREE.renderer. It doesn't make // sense to me that the camera and lights are in the derived class, but the scene and // renderer are in the base class. seems like inheritance-abuse. -class ThreeRenderer extends Renderer { +export default class ThreeRenderer extends Renderer { // constructor constructor() { @@ -54,6 +52,4 @@ class ThreeRenderer extends Renderer { removeObject(o) { this.scene.remove(o); } -} - -module.exports = ThreeRenderer; +} \ No newline at end of file diff --git a/src/render/aframe/system.js b/src/render/aframe/system.js index 9450c86..a98e114 100644 --- a/src/render/aframe/system.js +++ b/src/render/aframe/system.js @@ -1,9 +1,7 @@ -'use strict'; - const FRAME_HISTORY_SIZE = 20; const MAX_SLOW_FRAMES = 10; -let AFrameSystem = { +export default { schema: { traceLevel: { default: 4 } }, @@ -58,6 +56,4 @@ let AFrameSystem = { this.gameEngine = gameEngine; this.renderer = renderer; } -}; - -module.exports = AFrameSystem; +}; \ No newline at end of file diff --git a/src/render/pixi/PixiRenderableComponent.js b/src/render/pixi/PixiRenderableComponent.js new file mode 100644 index 0000000..39dc2b8 --- /dev/null +++ b/src/render/pixi/PixiRenderableComponent.js @@ -0,0 +1,46 @@ +import GameComponent from '../../serialize/GameComponent'; + +export default class PixiRenderableComponent extends GameComponent { + + constructor(options) { + super(); + this.options = options; + } + + /** + * Initial creation of the Pixi renderable + * @returns A pixi container/sprite + */ + createRenderable() { + let sprite; + if (this.options.assetName) { + sprite = new PIXI.Sprite(PIXI.loader.resources[this.options.assetName].texture); + } else if (this.options.spriteURL) { + sprite = new PIXI.Sprite.fromImage(this.options.spriteURL); + } + + if (this.options.width) { + sprite.width = this.options.width; + } + + if (this.options.height) { + sprite.height = this.options.height; + } + + if (this.options.onRenderableCreated) { + sprite = this.options.onRenderableCreated(sprite, this); + } + + return sprite; + } + + /** + * This method gets executed on every render step + * Note - this should only include rendering logic and not game logic + */ + render() { + if (this.options.onRender) { + this.options.onRender(); + } + } +} diff --git a/src/render/pixi/PixiRenderer.js b/src/render/pixi/PixiRenderer.js new file mode 100644 index 0000000..5e6e285 --- /dev/null +++ b/src/render/pixi/PixiRenderer.js @@ -0,0 +1,118 @@ +import Renderer from '../Renderer'; +import PixiRenderableComponent from './PixiRenderableComponent'; + +/** + * Pixi Renderer + */ +export default class PixiRenderer extends Renderer { + + /** + * Returns a dictionary of image assets and their paths + * E.G. { + ship: 'assets/ship.png', + missile: 'assets/missile.png', + } + * @returns {{}} + * @constructor + */ + get ASSETPATHS() { + return {}; + } + + constructor(gameEngine, clientEngine) { + super(gameEngine, clientEngine); + this.sprites = {}; + this.isReady = false; + } + + init() { + // prevent calling init twice + if (this.initPromise) return this.initPromise; + + this.viewportWidth = window.innerWidth; + this.viewportHeight = window.innerHeight; + this.stage = new PIXI.Container(); + + // default layers + this.layers = { + base: new PIXI.Container() + }; + + this.stage.addChild(this.layers.base); + + if (document.readyState === 'complete' || document.readyState === 'loaded' || document.readyState === 'interactive') { + this.onDOMLoaded(); + } else { + document.addEventListener('DOMContentLoaded', ()=>{ + this.onDOMLoaded(); + }); + } + + this.initPromise = new Promise((resolve, reject)=>{ + let onLoadComplete = () => { + this.isReady = true; + resolve(); + }; + + let resourceList = Object.keys(this.ASSETPATHS).map( x => { + return { + name: x, + url: this.ASSETPATHS[x] + }; + }); + + // make sure there are actual resources in the queue + if (resourceList.length > 0) + PIXI.loader.add(resourceList).load(onLoadComplete); + else + onLoadComplete(); + }); + + return this.initPromise; + } + + onDOMLoaded() { + this.renderer = PIXI.autoDetectRenderer(this.viewportWidth, this.viewportHeight); + document.body.querySelector('.pixiContainer').appendChild(this.renderer.view); + } + + draw() { + super.draw(); + + if (!this.isReady) return; // assets might not have been loaded yet + + for (let objId of Object.keys(this.sprites)) { + let objData = this.gameEngine.world.objects[objId]; + let sprite = this.sprites[objId]; + + if (objData) { + sprite.x = objData.position.x; + sprite.y = objData.position.y; + sprite.rotation = this.gameEngine.world.objects[objId].angle * Math.PI/180; + } + } + + this.renderer.render(this.stage); + } + + addObject(obj) { + if (obj.hasComponent(PixiRenderableComponent)){ + let renderable = obj.getComponent(PixiRenderableComponent); + let sprite = this.sprites[obj.id] = renderable.createRenderable(); + sprite.anchor.set(0.5, 0.5); + sprite.position.set(obj.position.x, obj.position.y); + this.layers.base.addChild(sprite); + } + } + + removeObject(obj) { + if (obj.hasComponent(PixiRenderableComponent)){ + let sprite = this.sprites[obj.id]; + if (sprite) { + this.sprites[obj.id].destroy(); + delete this.sprites[obj.id]; + } + } + } + +} diff --git a/src/serialize/DynamicObject.js b/src/serialize/DynamicObject.js index 1aa4062..95a4101 100644 --- a/src/serialize/DynamicObject.js +++ b/src/serialize/DynamicObject.js @@ -1,9 +1,7 @@ -'use strict'; - -const TwoVector = require('./TwoVector'); -const GameObject = require('./GameObject'); -const Serializer = require('./Serializer'); -const MathUtils = require('../lib/MathUtils'); +import TwoVector from './TwoVector'; +import GameObject from './GameObject'; +import Serializer from './Serializer'; +import MathUtils from '../lib/MathUtils'; /** * DynamicObject is the base class of the game's objects, for games which @@ -16,7 +14,7 @@ const MathUtils = require('../lib/MathUtils'); * allow the client to extrapolate the position * of dynamic objects in-between server updates. */ -class DynamicObject extends GameObject { +export default class DynamicObject extends GameObject { /** * The netScheme is a dictionary of attributes in this game @@ -44,6 +42,8 @@ class DynamicObject extends GameObject { return Object.assign({ playerId: { type: Serializer.TYPES.INT16 }, position: { type: Serializer.TYPES.CLASSINSTANCE }, + width: { type: Serializer.TYPES.INT16 }, + height: { type: Serializer.TYPES.INT16 }, velocity: { type: Serializer.TYPES.CLASSINSTANCE }, angle: { type: Serializer.TYPES.FLOAT32 } }, super.netScheme); @@ -51,17 +51,17 @@ class DynamicObject extends GameObject { /** * Creates an instance of a dynamic object. - * Override to provide starting values for position, velocity, etc. - * The object ID should be the next value provided by `world.idCount` - * @param {String} id - the object id - * @param {TwoVector} position - position vector - * @param {TwoVector} velocity - velocity vector - * @example - * // Ship is a subclass of DynamicObject: - * Ship(++this.world.idCount); + * NOTE: all subclasses of this class must comply with this constructor signature. + * This is required because the engine will create temporary instances when + * syncs arrive on the clients. + * @param {GameEngine} gameEngine - the gameEngine this object will be used in + * @param {Object} options - options for the new object. See {@link GameObject} + * @param {Object} props - properties to be set in the new object + * @param {TwoVector} props.position - position vector + * @param {TwoVector} props.velocity - velocity vector */ - constructor(id, position, velocity) { - super(id); + constructor(gameEngine, options, props) { + super(gameEngine, options); /** * ID of player who created this object @@ -72,6 +72,18 @@ class DynamicObject extends GameObject { this.position = new TwoVector(0, 0); this.velocity = new TwoVector(0, 0); + /** + * Object width for collision detection purposes. Default is 1 + * @member {Number} + */ + this.width = 1; + + /** + * Object Height for collision detection purposes. Default is 1 + * @member {Number} + */ + this.height = 1; + /** * The friction coefficient. Velocity is multiplied by this for each step. Default is (1,1) * @member {TwoVector} @@ -88,13 +100,13 @@ class DynamicObject extends GameObject { * position * @member {TwoVector} */ - if (position) this.position.copy(position); + if (props && props.position) this.position.copy(props.position); /** * velocity * @member {TwoVector} */ - if (velocity) this.velocity.copy(velocity); + if (props && props.velocity) this.velocity.copy(props.velocity); /** * object orientation angle in degrees @@ -150,7 +162,7 @@ class DynamicObject extends GameObject { */ toString() { function round3(x) { return Math.round(x * 1000) / 1000; } - return `dObj[${this.id}] player${this.playerId} Pos=${this.position} Vel=${this.velocity} angle${round3(this.angle)}`; + return `${this.constructor.name}[${this.id}] player${this.playerId} Pos=${this.position} Vel=${this.velocity} angle${round3(this.angle)}`; } /** @@ -261,6 +273,13 @@ class DynamicObject extends GameObject { this.angle = angle + shortestAngle * playPercentage; } -} -module.exports = DynamicObject; + getAABB() { + // todo take rotation into account + // registration point is in the middle + return { + min: [this.x - this.width / 2, this.y - this.height / 2], + max: [this.x + this.width / 2, this.y + this.height / 2] + }; + } +} diff --git a/src/serialize/GameComponent.js b/src/serialize/GameComponent.js new file mode 100644 index 0000000..d7ac280 --- /dev/null +++ b/src/serialize/GameComponent.js @@ -0,0 +1,18 @@ +export default class GameComponent { + + constructor(){ + /** + * the gameObject this component is attached to. This gets set in the addComponent method + * @member {GameObject} + */ + this.parentObject = null; + } + + static get name(){ + return this.constructor.name; + } + + static get netScheme(){ + return null; + } +} \ No newline at end of file diff --git a/src/serialize/GameObject.js b/src/serialize/GameObject.js index d22a110..719deb9 100644 --- a/src/serialize/GameObject.js +++ b/src/serialize/GameObject.js @@ -1,7 +1,5 @@ -'use strict'; - -const Serializable = require('./Serializable'); -const Serializer = require('./Serializer'); +import Serializable from './Serializable'; +import Serializer from './Serializer'; /** * GameObject is the base class of all game objects. @@ -10,7 +8,7 @@ const Serializer = require('./Serializer'); * Game developers will use one of the subclasses such as DynamicObject, * or PhysicalObject. */ -class GameObject extends Serializable { +export default class GameObject extends Serializable { static get netScheme() { return { @@ -20,37 +18,54 @@ class GameObject extends Serializable { /** * Creates an instance of a game object. - * @param {String} id - the object id + * @param {GameEngine} gameEngine - the gameEngine this object will be used in + * @param {Object} options - options for instantiation of the GameObject + * @param {Number} id - if set, the new instantiated object will be set to this id instead of being generated a new one. Use with caution! */ - constructor(id) { - + constructor(gameEngine, options) { super(); + /** + * The gameEngine this object will be used in + * @member {GameEngine} + */ + this.gameEngine = gameEngine; /** - * ID of this object's instance. Each instance has an ID which is unique across the entire - * game world, including the server and all the clients. In extrapolation mode, - * the client may have an object instance which does not yet exist on the server, - * these objects are known as shadow objects. + * ID of this object's instance. + * There are three cases of instance creation which can occur: + * 1. In the normal case, the constructor is asked to assign an ID which is unique + * across the entire game world, including the server and all the clients. + * 2. In extrapolation mode, the client may have an object instance which does not + * yet exist on the server, these objects are known as shadow objects. Their IDs must + * be allocated from a different range. + * 3. Also, temporary objects are created on the client side each time a sync is received. + * These are used for interpolation purposes and as bending targets of position, velocity, + * angular velocity, and orientation. In this case the id will be set to null. * @member {Number} */ - this.id = id; + this.id = null; + if (options && 'id' in options) + this.id = options.id; + else if (this.gameEngine) + this.id = this.gameEngine.world.getNewId(); + + this.components = {}; } /** - * Initialize the object. - * Extend this method if you have object initialization logic. - * @param {Object} options Your object's options + * Called after the object is added to to the game world. + * This is the right place to add renderer sub-objects, physics sub-objects + * and any other resources that should be created + * @param {GameEngine} gameEngine the game engine */ - init(options) { - Object.assign(this, options); - } + onAddToWorld(gameEngine) {} /** - * Add this object to the game-world by creating physics sub-objects - * renderer sub-objects and any other resources + * Called after the object is removed from game-world. + * This is where renderer sub-objects and any other resources should be freed * @param {GameEngine} gameEngine the game engine */ - onAddToWorld(gameEngine) {} + onRemoveFromWorld(gameEngine) {} /** * Formatted textual description of the game object. @@ -69,7 +84,7 @@ class GameObject extends Serializable { } saveState(other) { - this.savedCopy = (new this.constructor()); + this.savedCopy = (new this.constructor(this.gameEngine, { id: null })); this.savedCopy.syncTo(other ? other : this); } @@ -123,11 +138,43 @@ class GameObject extends Serializable { // copy physical attributes from physics sub-object refreshFromPhysics() {} + // apply a single bending increment + applyIncrementalBending() { } + // clean up resources destroy() {} - // apply a single bending increment - applyIncrementalBending() { } -}; + addComponent(componentInstance) { + componentInstance.parentObject = this; + this.components[componentInstance.constructor.name] = componentInstance; + + // a gameEngine might not exist if this class is instantiated by the serializer + if (this.gameEngine) { + this.gameEngine.emit('componentAdded', this, componentInstance); + } + } + + removeComponent(componentName) { + // todo cleanup of the component ? + delete this.components[componentName]; + + // a gameEngine might not exist if this class is instantiated by the serializer + if (this.gameEngine) { + this.gameEngine.emit('componentRemoved', this, componentName); + } + } + + /** + * Check whether this game object has a certain component + * @param componentClass the comp + * @returns {Boolean} true if the gameObject contains this component + */ + hasComponent(componentClass) { + return componentClass.name in this.components; + } + + getComponent(componentClass) { + return this.components[componentClass.name]; + } -module.exports = GameObject; +} diff --git a/src/serialize/PhysicalObject.js b/src/serialize/PhysicalObject.js index f239338..70368d5 100644 --- a/src/serialize/PhysicalObject.js +++ b/src/serialize/PhysicalObject.js @@ -1,14 +1,12 @@ -'use strict'; - -const GameObject = require('./GameObject'); -const Serializer = require('./Serializer'); -const ThreeVector = require('./ThreeVector'); -const Quaternion = require('./Quaternion'); +import GameObject from './GameObject'; +import Serializer from './Serializer'; +import ThreeVector from './ThreeVector'; +import Quaternion from './Quaternion'; /** * The PhysicalObject is the base class for physical game objects */ -class PhysicalObject extends GameObject { +export default class PhysicalObject extends GameObject { // TODO: // this code is not performance optimized, generally speaking. @@ -52,14 +50,19 @@ class PhysicalObject extends GameObject { * Creates an instance of a physical object. * Override to provide starting values for position, velocity, quaternion and angular velocity. * The object ID should be the next value provided by `world.idCount` - * @param {String} id - the object id - * @param {ThreeVector} position - position vector - * @param {ThreeVector} velocity - velocity vector - * @param {Quaternion} quaternion - orientation quaternion - * @param {ThreeVector} angularVelocity - 3-vector representation of angular velocity + * NOTE: all subclasses of this class must comply with this constructor signature. + * This is required because the engine will create temporary instances when + * syncs arrive on the clients. + * @param {GameEngine} gameEngine - the gameEngine this object will be used in + * @param {Object} options - options for the new object. See {@link GameObject} + * @param {Object} props - properties to be set in the new object + * @param {ThreeVector} props.position - position vector + * @param {ThreeVector} props.velocity - velocity vector + * @param {Quaternion} props.quaternion - orientation quaternion + * @param {ThreeVector} props.angularVelocity - 3-vector representation of angular velocity */ - constructor(id, position, velocity, quaternion, angularVelocity) { - super(id); + constructor(gameEngine, options, props) { + super(gameEngine, options); this.playerId = 0; this.bendingIncrements = 0; @@ -70,10 +73,11 @@ class PhysicalObject extends GameObject { this.angularVelocity = new ThreeVector(0, 0, 0); // use values if provided - if (position) this.position.copy(position); - if (velocity) this.velocity.copy(velocity); - if (quaternion) this.quaternion.copy(quaternion); - if (angularVelocity) this.angularVelocity.copy(angularVelocity); + props = props || {}; + if (props.position) this.position.copy(props.position); + if (props.velocity) this.velocity.copy(props.velocity); + if (props.quaternion) this.quaternion.copy(props.quaternion); + if (props.angularVelocity) this.angularVelocity.copy(props.angularVelocity); this.class = PhysicalObject; } @@ -109,6 +113,11 @@ class PhysicalObject extends GameObject { this.bendingPositionDelta.subtract(original.position); this.bendingPositionDelta.multiplyScalar(this.incrementScale); + // get the incremental angular-velocity + this.bendingAVDelta = (new ThreeVector()).copy(this.angularVelocity); + this.bendingAVDelta.subtract(original.angularVelocity); + this.bendingAVDelta.multiplyScalar(this.incrementScale); + // get the incremental quaternion rotation let currentConjugate = (new Quaternion()).copy(original.quaternion).conjugate(); this.bendingQuaternionDelta = (new Quaternion()).copy(this.quaternion); @@ -119,7 +128,7 @@ class PhysicalObject extends GameObject { this.bendingTarget = (new this.constructor()); this.bendingTarget.syncTo(this); - this.syncTo(original, { keepVelocities: true }); + this.syncTo(original, { keepVelocity: true }); this.bendingIncrements = bendingIncrements; this.bending = bending; @@ -136,10 +145,10 @@ class PhysicalObject extends GameObject { this.position.copy(other.position); this.quaternion.copy(other.quaternion); + this.angularVelocity.copy(other.angularVelocity); - if (!options || !options.keepVelocities) { + if (!options || !options.keepVelocity) { this.velocity.copy(other.velocity); - this.angularVelocity.copy(other.angularVelocity); } if (this.physicsObj) @@ -163,16 +172,46 @@ class PhysicalObject extends GameObject { } // apply one increment of bending - applyIncrementalBending() { + applyIncrementalBending(stepDesc) { if (this.bendingIncrements === 0) return; - this.position.add(this.bendingPositionDelta); - this.quaternion.slerp(this.bendingTarget.quaternion, this.incrementScale); + if (stepDesc && stepDesc.dt) { + const timeFactor = stepDesc.dt / (1000 / 60); + const posDelta = (new ThreeVector()).copy(this.bendingPositionDelta).multiplyScalar(timeFactor); + const avDelta = (new ThreeVector()).copy(this.bendingAVDelta).multiplyScalar(timeFactor); + this.position.add(posDelta); + this.angularVelocity.add(avDelta); + + // TODO: this is an unacceptable workaround that must be removed. It solves the + // jitter problem by applying only three steps of slerp (thus avoiding slerp to back in time + // instead of solving the problem with a true differential quaternion + if (this.bendingIncrements > 3) { + this.quaternion.slerp(this.bendingTarget.quaternion, this.incrementScale * timeFactor * 0.6); + } + } else { + this.position.add(this.bendingPositionDelta); + this.angularVelocity.add(this.bendingAVDelta); + this.quaternion.slerp(this.bendingTarget.quaternion, this.incrementScale); + } + // TODO: the following approach is encountering gimbal lock // this.quaternion.multiply(this.bendingQuaternionDelta); this.bendingIncrements--; } -} -module.exports = PhysicalObject; + // interpolate implementation + interpolate(nextObj, percent, worldSettings) { + + // get the incremental delta position + // const positionDelta = (new ThreeVector()) + // .copy(nextObj.position) + // .subtract(this.position) + // .multiplyScalar(playPercentage); + // this.position.add(positionDelta); + + // slerp to target position + this.position.lerp(nextObj.position, percent); + this.quaternion.slerp(nextObj.quaternion, percent); + } +} diff --git a/src/serialize/Quaternion.js b/src/serialize/Quaternion.js index 617f96f..d527e20 100644 --- a/src/serialize/Quaternion.js +++ b/src/serialize/Quaternion.js @@ -1,14 +1,12 @@ -'use strict'; - -const Serializable = require('./Serializable'); -const Serializer = require('./Serializer'); -const ThreeVector = require('./ThreeVector'); +import Serializable from './Serializable'; +import Serializer from './Serializer'; +import ThreeVector from './ThreeVector'; /** * A Quaternion is a geometric object which can be used to * represent a three-dimensional rotation. */ -class Quaternion extends Serializable { +export default class Quaternion extends Serializable { static get netScheme() { return { @@ -191,6 +189,4 @@ class Quaternion extends Serializable { return this; } /* eslint-enable */ -} - -module.exports = Quaternion; +} \ No newline at end of file diff --git a/src/serialize/Serializable.js b/src/serialize/Serializable.js index 0ba044c..aa47f3e 100644 --- a/src/serialize/Serializable.js +++ b/src/serialize/Serializable.js @@ -1,9 +1,7 @@ -'use strict'; +import Utils from './../lib/Utils'; +import Serializer from './Serializer'; -const Utils = require('./../lib/Utils'); -const Serializer = require('./Serializer'); - -class Serializable { +export default class Serializable { /** * Class can be serialized using either: - a class based netScheme @@ -122,10 +120,10 @@ class Serializable { let isString = p => netScheme[p].type === Serializer.TYPES.STRING; let hasChanged = p => prevObject[p] !== this[p]; let changedStrings = Object.keys(netScheme).filter(isString).filter(hasChanged); - if (!changedStrings) return this; + if (changedStrings.length == 0) return this; // build a clone with pruned strings - let prunedCopy = new this.constructor(); + let prunedCopy = new this.constructor(null, { id: null }); for (let p of Object.keys(netScheme)) prunedCopy[p] = changedStrings.indexOf(p) < 0 ? this[p] : null; @@ -152,5 +150,3 @@ class Serializable { } } - -module.exports = Serializable; diff --git a/src/serialize/Serializer.js b/src/serialize/Serializer.js index e2a4c45..e953eda 100644 --- a/src/serialize/Serializer.js +++ b/src/serialize/Serializer.js @@ -1,6 +1,8 @@ -'use strict'; +import Utils from './../lib/Utils'; +import TwoVector from './TwoVector'; +import ThreeVector from './ThreeVector'; +import Quaternion from './Quaternion'; -const Utils = require('./../lib/Utils'); const MAX_UINT_16 = 0xFFFF; /** @@ -10,15 +12,14 @@ const MAX_UINT_16 = 0xFFFF; * * The Serializer defines the data types which can be serialized. */ -class Serializer { +export default class Serializer { constructor() { this.registeredClasses = {}; this.customTypes = {}; - this.netSchemeSizeCache = {}; // used to cache calculated netSchemes sizes - this.registerClass(require('./TwoVector')); - this.registerClass(require('./ThreeVector')); - this.registerClass(require('./Quaternion')); + this.registerClass(TwoVector); + this.registerClass(ThreeVector); + this.registerClass(Quaternion); } /** @@ -72,7 +73,8 @@ class Serializer { localByteOffset += Uint8Array.BYTES_PER_ELEMENT; // advance the byteOffset after the classId - let obj = new objectClass(); + // create de-referenced instance of the class. gameEngine and id will be 'tacked on' later at the sync strategies + let obj = new objectClass(null, {id: null}); for (let property of Object.keys(objectClass.netScheme).sort()) { let read = this.readDataView(dataView, byteOffset + localByteOffset, objectClass.netScheme[property]); obj[property] = read.data; @@ -247,6 +249,4 @@ Serializer.TYPES = { STRING: 'STRING', CLASSINSTANCE: 'CLASSINSTANCE', LIST: 'LIST' -}; - -module.exports = Serializer; +}; \ No newline at end of file diff --git a/src/serialize/THREEPhysicalObject.js b/src/serialize/THREEPhysicalObject.js index 284ee9c..36cd719 100644 --- a/src/serialize/THREEPhysicalObject.js +++ b/src/serialize/THREEPhysicalObject.js @@ -1,9 +1,6 @@ -"use strict"; +import PhysicalObject from './PhysicalObject'; -const PhysicalObject = require('./PhysicalObject'); - - -class THREEPhysicalObject extends PhysicalObject { +export default class THREEPhysicalObject extends PhysicalObject { static get properties() { return { @@ -48,6 +45,4 @@ class THREEPhysicalObject extends PhysicalObject { this.renderObject.rotation.set(this.rx, this.ry, this.rz); } -} - -module.exports = THREEPhysicalObject; +} \ No newline at end of file diff --git a/src/serialize/ThreeVector.js b/src/serialize/ThreeVector.js index eee5cb8..682bf6a 100644 --- a/src/serialize/ThreeVector.js +++ b/src/serialize/ThreeVector.js @@ -1,13 +1,11 @@ -'use strict'; - -const Serializable = require('./Serializable'); -const Serializer = require('./Serializer'); +import Serializable from './Serializable'; +import Serializer from './Serializer'; /** * A ThreeVector is a geometric object which is completely described * by three values. */ -class ThreeVector extends Serializable { +export default class ThreeVector extends Serializable { static get netScheme() { return { @@ -142,5 +140,3 @@ class ThreeVector extends Serializable { return this; } } - -module.exports = ThreeVector; diff --git a/src/serialize/TwoVector.js b/src/serialize/TwoVector.js index 9ddcafb..c3a5083 100644 --- a/src/serialize/TwoVector.js +++ b/src/serialize/TwoVector.js @@ -1,13 +1,11 @@ -'use strict'; - -const Serializable = require('./Serializable'); -const Serializer = require('./Serializer'); +import Serializable from './Serializable'; +import Serializer from './Serializer'; /** * A TwoVector is a geometric object which is completely described * by two values. */ -class TwoVector extends Serializable { +export default class TwoVector extends Serializable { static get netScheme() { return { @@ -98,5 +96,3 @@ class TwoVector extends Serializable { this.y += (target.y - this.y) * p; } } - -module.exports = TwoVector; diff --git a/src/syncStrategies/ExtrapolateStrategy.js b/src/syncStrategies/ExtrapolateStrategy.js index 45ca07e..e2e51bf 100644 --- a/src/syncStrategies/ExtrapolateStrategy.js +++ b/src/syncStrategies/ExtrapolateStrategy.js @@ -1,6 +1,4 @@ -'use strict'; - -const SyncStrategy = require('./SyncStrategy'); +import SyncStrategy from './SyncStrategy'; const defaults = { syncsBufferLength: 5, @@ -12,7 +10,7 @@ const defaults = { bendingIncrements: 10 // the bending should be applied increments (how many steps for entire bend) }; -class ExtrapolateStrategy extends SyncStrategy { +export default class ExtrapolateStrategy extends SyncStrategy { constructor(clientEngine, inputOptions) { @@ -24,7 +22,6 @@ class ExtrapolateStrategy extends SyncStrategy { this.recentInputs = {}; this.gameEngine = this.clientEngine.gameEngine; this.gameEngine.on('client__postStep', this.extrapolate.bind(this)); - this.gameEngine.on('client__syncReceived', this.collectSync.bind(this)); this.gameEngine.on('client__processInput', this.clientInputSave.bind(this)); } @@ -38,54 +35,12 @@ class ExtrapolateStrategy extends SyncStrategy { this.recentInputs[inputData.step].push(inputData); } - // collect a sync and its events - collectSync(e) { - - // on first connect we need to wait for a full world update - if (this.needFirstSync) { - if (!e.fullUpdate) - return; - } else { - // ignore syncs which are older than the latest - if (this.lastSync && this.lastSync.stepCount && this.lastSync.stepCount > e.stepCount) - return; - } - - // build new sync object - 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`); - } - // add an object to our world addNewObject(objId, newObj, options) { - let curObj = new newObj.constructor(); + let curObj = new newObj.constructor(this.gameEngine, { + id: objId + }); curObj.syncTo(newObj); this.gameEngine.addObjectToWorld(curObj); console.log(`adding new object ${curObj}`); @@ -106,7 +61,7 @@ class ExtrapolateStrategy extends SyncStrategy { // apply a new sync applySync() { - this.gameEngine.trace.debug('extrapolate applying sync'); + this.gameEngine.trace.debug(() => 'extrapolate applying sync'); // // scan all the objects in the sync @@ -132,9 +87,8 @@ class ExtrapolateStrategy extends SyncStrategy { let localShadowObj = this.gameEngine.findLocalShadow(ev.objectInstance); if (localShadowObj) { - // case 1: this object has a local shadow object on the client - this.gameEngine.trace.debug(`object ${ev.objectInstance.id} replacing local shadow ${localShadowObj.id}`); + this.gameEngine.trace.debug(() => `object ${ev.objectInstance.id} replacing local shadow ${localShadowObj.id}`); if (!world.objects.hasOwnProperty(ev.objectInstance.id)) { let newObj = this.addNewObject(ev.objectInstance.id, ev.objectInstance, { visible: false }); @@ -145,10 +99,10 @@ class ExtrapolateStrategy extends SyncStrategy { } else if (curObj) { // case 2: this object already exists locally - this.gameEngine.trace.trace(`object before syncTo: ${curObj.toString()}`); + this.gameEngine.trace.trace(() => `object before syncTo: ${curObj.toString()}`); curObj.saveState(); curObj.syncTo(ev.objectInstance); - this.gameEngine.trace.trace(`object after syncTo: ${curObj.toString()} synced to step[${ev.stepCount}]`); + this.gameEngine.trace.trace(() => `object after syncTo: ${curObj.toString()} synced to step[${ev.stepCount}]`); } else { @@ -161,10 +115,10 @@ class ExtrapolateStrategy extends SyncStrategy { // reenact the steps that we want to extrapolate forwards // this.cleanRecentInputs(); - this.gameEngine.trace.debug(`extrapolate re-enacting steps from [${serverStep}] to [${world.stepCount}]`); + this.gameEngine.trace.debug(() => `extrapolate re-enacting steps from [${serverStep}] to [${world.stepCount}]`); if (serverStep < world.stepCount - this.options.maxReEnactSteps) { serverStep = world.stepCount - this.options.maxReEnactSteps; - this.gameEngine.trace.info(`too many steps to re-enact. Starting from [${serverStep}] to [${world.stepCount}]`); + this.gameEngine.trace.info(() => `too many steps to re-enact. Starting from [${serverStep}] to [${world.stepCount}]`); } let clientStep = world.stepCount; @@ -175,8 +129,8 @@ class ExtrapolateStrategy extends SyncStrategy { // only movement inputs are re-enacted if (!inputData.inputOptions || !inputData.inputOptions.movement) return; - this.gameEngine.trace.trace(`extrapolate re-enacting movement input[${inputData.messageIndex}]: ${inputData.input}`); - this.gameEngine.processInput(inputData, this.clientEngine.playerId); + this.gameEngine.trace.trace(() => `extrapolate re-enacting movement input[${inputData.messageIndex}]: ${inputData.input}`); + this.gameEngine.processInput(inputData, this.gameEngine.playerId); }); } @@ -198,17 +152,17 @@ class ExtrapolateStrategy extends SyncStrategy { // Reminder: the reason we use a string is that these // values are sometimes used as object keys let obj = world.objects[objId]; - let isLocal = (obj.playerId == this.clientEngine.playerId); // eslint-disable-line eqeqeq + let isLocal = (obj.playerId == this.gameEngine.playerId); // eslint-disable-line eqeqeq let bending = isLocal ? this.options.localObjBending : this.options.remoteObjBending; obj.bendToCurrentState(bending, this.gameEngine.worldSettings, isLocal, this.options.bendingIncrements); if (typeof obj.refreshRenderObject === 'function') obj.refreshRenderObject(); - this.gameEngine.trace.trace(`object[${objId}] ${obj.bendingToString()}`); + this.gameEngine.trace.trace(() => `object[${objId}] ${obj.bendingToString()}`); } // trace object state after sync for (let objId of Object.keys(world.objects)) { - this.gameEngine.trace.trace(`object after extrapolate replay: ${world.objects[objId].toString()}`); + this.gameEngine.trace.trace(() => `object after extrapolate replay: ${world.objects[objId].toString()}`); } // destroy objects @@ -226,13 +180,14 @@ class ExtrapolateStrategy extends SyncStrategy { } // Perform client-side extrapolation. - extrapolate() { + extrapolate(stepDesc) { // apply incremental bending this.gameEngine.world.forEachObject((id, o) => { if (typeof o.applyIncrementalBending === 'function') { - o.applyIncrementalBending(); + o.applyIncrementalBending(stepDesc); o.refreshToPhysics(); + // this.gameEngine.trace.trace(() => `object[${id}] after bending : ${o.toString()}`); } }); @@ -243,5 +198,3 @@ class ExtrapolateStrategy extends SyncStrategy { } } } - -module.exports = ExtrapolateStrategy; diff --git a/src/syncStrategies/FrameSyncStrategy.js b/src/syncStrategies/FrameSyncStrategy.js index 45af98a..0f6bb8a 100644 --- a/src/syncStrategies/FrameSyncStrategy.js +++ b/src/syncStrategies/FrameSyncStrategy.js @@ -1,13 +1,11 @@ -"use strict"; - -const SyncStrategy = require("./SyncStrategy"); +import SyncStrategy from './SyncStrategy' const defaults = { worldBufferLength: 60, clientStepLag: 0 }; -class FrameSyncStrategy extends SyncStrategy { +export default class FrameSyncStrategy extends SyncStrategy { constructor(clientEngine, inputOptions) { @@ -76,6 +74,4 @@ class FrameSyncStrategy extends SyncStrategy { } } } -} - -module.exports = FrameSyncStrategy; +} \ No newline at end of file diff --git a/src/syncStrategies/InterpolateStrategy.js b/src/syncStrategies/InterpolateStrategy.js index fc4f2b7..6605c08 100644 --- a/src/syncStrategies/InterpolateStrategy.js +++ b/src/syncStrategies/InterpolateStrategy.js @@ -1,6 +1,4 @@ -'use strict'; - -const SyncStrategy = require('./SyncStrategy'); +import SyncStrategy from './SyncStrategy'; const defaults = { syncsBufferLength: 6, @@ -8,7 +6,7 @@ const defaults = { reflect: false }; -class InterpolateStrategy extends SyncStrategy { +export default class InterpolateStrategy extends SyncStrategy { constructor(clientEngine, inputOptions) { @@ -19,45 +17,16 @@ class InterpolateStrategy extends SyncStrategy { 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); - }); + super.collectSync(e); - 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`); + if (!this.lastSync) + return; - this.syncsBuffer.push(lastSync); + this.syncsBuffer.push(this.lastSync); if (this.syncsBuffer.length >= this.options.syncsBufferLength) { this.syncsBuffer.shift(); } @@ -66,18 +35,14 @@ class InterpolateStrategy extends SyncStrategy { // add an object to our world addNewObject(objId, newObj, stepCount) { - let curObj = new newObj.constructor(); + let curObj = new newObj.constructor(this.gameEngine, { + id: objId + }); 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; } @@ -105,12 +70,12 @@ class InterpolateStrategy extends SyncStrategy { // we requires a sync before we proceed if (!nextSync) { - this.gameEngine.trace.debug('interpolate lacks future sync - requesting step skip'); + 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}`); + 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]; @@ -183,22 +148,12 @@ class InterpolateStrategy extends SyncStrategy { // so the code below should be easy to simplify now interpolateOneObject(prevObj, nextObj, objId, playPercentage) { - // handle step for this object + // update position and orientation with interpolation 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()}`); + 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(); - } + this.gameEngine.trace.trace(() => `object ${objId} after interpolate: ${curObj.toString()}`); } } } - -module.exports = InterpolateStrategy; diff --git a/src/syncStrategies/SyncStrategy.js b/src/syncStrategies/SyncStrategy.js index 4eea4dc..7b933f2 100644 --- a/src/syncStrategies/SyncStrategy.js +++ b/src/syncStrategies/SyncStrategy.js @@ -1,14 +1,67 @@ -"use strict"; +'use strict'; -// TODO: make this class non-trivial, or remove it -class SyncStrategy { +export default class SyncStrategy { constructor(clientEngine, inputOptions) { this.clientEngine = clientEngine; this.gameEngine = clientEngine.gameEngine; this.options = Object.assign({}, inputOptions); + 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; + } + + // build new sync object + let lastSync = this.lastSync = { + stepCount: e.stepCount, + syncObjects: {}, + syncSteps: {} + }; + + e.syncEvents.forEach(sEvent => { -module.exports = SyncStrategy; + // 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, + eventName = sEvent.eventName; + + 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`); + } +} diff --git a/test/EndToEnd/testGame/src/client/MyRenderer.js b/test/EndToEnd/testGame/src/client/MyRenderer.js index a736d4d..46620b9 100644 --- a/test/EndToEnd/testGame/src/client/MyRenderer.js +++ b/test/EndToEnd/testGame/src/client/MyRenderer.js @@ -16,9 +16,9 @@ class MyRenderer extends Renderer { addObject(objData, options) { let sprite = {}; - + // add this object to the renderer: - // if (objData.class == PlayerAvatar) { + // if (objData instanceof PlayerAvatar) { // ... // } diff --git a/test/SimplePhysics/HSHG.js b/test/SimplePhysics/HSHG.js new file mode 100644 index 0000000..48fc849 --- /dev/null +++ b/test/SimplePhysics/HSHG.js @@ -0,0 +1,69 @@ +'use strict' + +const should = require('should'); +const Serializer = require('../../src/serialize/Serializer'); +const DynamicObject = require('../../src/serialize/DynamicObject'); +const HSHG = require('../../src/physics/SimplePhysics/HSHG'); + +class TestObject extends DynamicObject { + + static get netScheme(){ + return { + height: Serializer.TYPES.UINT16, + width: Serializer.TYPES.UINT16 + }; + } + + constructor(){ + super(); + this.width = 100; + this.height = 100; + }; +} + +let grid = new HSHG(); + +let obj1 = new TestObject(1); +let obj2 = new TestObject(2); + +grid.addObject(obj1); +grid.addObject(obj2); + +describe('HSHG collision detection', function() { + + it('No collision 1', function() { + obj1.position.x = 0; + obj2.position.x = 101; + grid.update(); + let collisionPairs = grid.queryForCollisionPairs(); + collisionPairs.length.should.equal(0); + }); + + it('Partial overlap Collision', function() { + obj1.position.x = 0; + obj2.position.x = 50; + grid.update(); + let collisionPairs = grid.queryForCollisionPairs(); + collisionPairs.length.should.equal(1); + }); + + it('No collision 2', function() { + obj1.position.x = 0; + obj2.position.x = 101; + grid.update(); + let collisionPairs = grid.queryForCollisionPairs(); + collisionPairs.length.should.equal(0); + }); + + + it('Full overlap collision', function() { + obj1.position.x = 0; + obj2.position.x = 0; + obj2.position.width = 50; + obj2.position.height = 50; + grid.update(); + let collisionPairs = grid.queryForCollisionPairs(); + collisionPairs.length.should.equal(1); + }); + +}); diff --git a/test/SimplePhysics/HSHGGameEngine.js b/test/SimplePhysics/HSHGGameEngine.js new file mode 100644 index 0000000..db66853 --- /dev/null +++ b/test/SimplePhysics/HSHGGameEngine.js @@ -0,0 +1,95 @@ +'use strict' + +const should = require('should'); +const Serializer = require('../../src/serialize/Serializer'); +const DynamicObject = require('../../src/serialize/DynamicObject'); +const GameEngine = require('../../src/GameEngine'); +const SimplePhysicsEngine = require('../../src/physics/SimplePhysicsEngine'); + +class TestObject extends DynamicObject { + + static get netScheme(){ + return Object.assign({ + height: Serializer.TYPES.UINT16, + width: Serializer.TYPES.UINT16 + }, super.netScheme); + } + + constructor(id){ + super(id); + this.width = 10; + this.height = 10; + }; +} + +let gameEngine = new GameEngine(); +gameEngine.physicsEngine = new SimplePhysicsEngine({ + gameEngine: gameEngine, + collisionOptions: { + type: 'HSHG' + } +}); + +gameEngine.start(); + +let obj1 = new TestObject(1); +let obj2 = new TestObject(2); +gameEngine.addObjectToWorld(obj1); +gameEngine.addObjectToWorld(obj2); + +describe('HSHG Game Engine collision detection', function() { + obj1.position.x = 5; + obj2.position.x = 20; + obj2.velocity.x = -5; + console.log(obj1.position, obj2.position); + + + it('Step 0 - No collision ', function() { + // console.log(obj1.position, obj2.position); + gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(false); + }); + + it('Step 1 - collision ', function() { + gameEngine.once('collisionStart', pairObj => { + gameEngine.world.stepCount.should.equal(1); + }); + gameEngine.step(false); + console.log(obj1.position, obj2.position); + gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true); + }); + + it('Step 2 - collision ', function() { + gameEngine.step(false); + console.log(obj1.position, obj2.position); + gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true); + }); + + + it('Step 3 - collision ', function() { + gameEngine.step(false); + console.log(obj1.position, obj2.position); + gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true); + }); + + it('Step 4 - collision ', function() { + gameEngine.step(false); + console.log(obj1.position, obj2.position); + gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true); + }); + + it('Step 5 - collision ', function() { + gameEngine.step(false); + console.log(obj1.position, obj2.position); + gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true); + }); + + it('Step 6 - no collision ', function() { + gameEngine.once('collisionStop', pairObj => { + gameEngine.world.stepCount.should.equal(6); + }); + console.log(obj1.position, obj2.position); + gameEngine.step(false); + gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(false); + }); + +}); diff --git a/test/serializer/list.js b/test/serializer/list.js index c678c43..f031e3a 100644 --- a/test/serializer/list.js +++ b/test/serializer/list.js @@ -1,45 +1,42 @@ -"use strict"; - -const should = require('should'); - -const Serializable = require('../../src/serialize/Serializable'); -const Serializer = require('../../src/serialize//Serializer'); +// Serializer must be loaded first before Serializable because of circular deps +import Serializer from '../../src/serialize/Serializer'; +import Serializable from '../../src/serialize/Serializable'; class TestObject extends Serializable { - static get netScheme(){ + static get netScheme() { return { playerAges: { type: Serializer.TYPES.LIST, itemType: Serializer.TYPES.UINT8 }, - } + }; } - constructor(playerAges){ + constructor(playerAges) { super(); this.playerAges = playerAges; - }; + } } var serializer = new Serializer(); -var testObject = new TestObject([1,2,3]); +var testObject = new TestObject([1, 2, 3]); serializer.registerClass(TestObject); testObject.class = TestObject; describe('List serialization/deserialization', function() { - let serializedTestObject, deserializedTestObject; - + let serializedTestObject = null; + let deserializedTestObject = null; describe('primitives', function() { - it('Serialize list', function () { + it('Serialize list', function() { serializedTestObject = testObject.serialize(serializer); }); - it('Deserialize list', function () { + it('Deserialize list', function() { deserializedTestObject = serializer.deserialize(serializedTestObject.dataBuffer); deserializedTestObject.byteOffset.should.equal(6); deserializedTestObject.obj.playerAges.should.be.instanceof(Array).and.have.lengthOf(3); diff --git a/test/serializer/primitives.js b/test/serializer/primitives.js index f6eb47f..2081038 100644 --- a/test/serializer/primitives.js +++ b/test/serializer/primitives.js @@ -1,9 +1,7 @@ -"use strict"; +import should from 'should'; -const should = require('should'); - -const Serializable = require('../../src/serialize/Serializable'); -const Serializer = require('../../src/serialize//Serializer'); +import Serializer from '../../src/serialize//Serializer'; +import Serializable from '../../src/serialize/Serializable'; class TestObject extends Serializable { diff --git a/test/serializer/string.js b/test/serializer/string.js index dc3188d..e8ccf47 100644 --- a/test/serializer/string.js +++ b/test/serializer/string.js @@ -1,9 +1,7 @@ -'use strict'; +import should from 'should'; -const should = require('should'); - -const Serializable = require('../../src/serialize/Serializable'); -const Serializer = require('../../src/serialize//Serializer'); +import Serializer from '../../src/serialize//Serializer'; +import Serializable from '../../src/serialize/Serializable'; class TestObject extends Serializable { @@ -49,4 +47,4 @@ describe('List serialization/deserialization', function() { }); -}); +}); \ No newline at end of file diff --git a/utils/showMovement.js b/utils/showMovement.js index 4b8d008..513e4d3 100644 --- a/utils/showMovement.js +++ b/utils/showMovement.js @@ -2,10 +2,10 @@ const fs = require('fs'); const process = require('process'); // -// this little utility shows the deltas between positions on each step +// this little utility shows the deltas between positions, quaternions, and time on each step // let FILENAME = 'server.trace'; -let positions = {}; +let states = {}; if (process.argv.length === 3) FILENAME = process.argv[2]; let fin = fs.readFileSync(FILENAME); @@ -13,13 +13,24 @@ let lines = fin.toString().split('\n'); for (let l of lines) { if (l.indexOf('after step') < 0) continue; + // match: Pos=(0, -15.4, 0) let p = l.match(/Pos=\(([0-9.-]*), ([0-9.-]*), ([0-9.-]*)\)/); + + // match: quaternion=(1, 0, 0, -0.01) + let q = l.match(/quaternion\(([0-9.-]*), ([0-9.-]*), ([0-9.-]*), ([0-9.-]*)\)/); + + // match: 2017-06-01T14:25:24.197Z + let ts = l.match(/^\[([0-9\-T:.Z]*)\]/); + let t = new Date(ts[1]); + let step = l.match(/([0-9]*>)/); let parts = l.split(' '); let objname = parts[4]; - let oldp = positions[objname]; - if (oldp) { - console.log(`step ${step[1]} object ${objname} moved (${Number(p[1]) - Number(oldp[1])},${Number(p[2]) - Number(oldp[2])},${Number(p[3]) - Number(oldp[3])})}`); + let old = states[objname]; + if (old) { + let deltaP = `(${Number(p[1]) - Number(old.p[1])},${Number(p[2]) - Number(old.p[2])},${Number(p[3]) - Number(old.p[3])})`; + let deltaQ = `(${Number(q[1]) - Number(old.q[1])},${Number(q[2]) - Number(old.q[2])},${Number(q[3]) - Number(old.q[3])},${Number(q[4]) - Number(old.q[4])})`; + console.log(`step ${step[1]} dt=${t - old.t} object ${objname} moved ${deltaP} rotated ${deltaQ}`); } - positions[objname] = p; + states[objname] = { p, q, t }; }