diff --git a/.gitignore b/.gitignore index 34d99e1..618f339 100755 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ lab/audio/ lab/filter/ static/items/rube/*-backups build/ +runs/ +poc-webrtc/runs/ +poc-webrtc/node_modules/ diff --git a/app/Game/Channel/Control/PlayerController.js b/app/Game/Channel/Control/PlayerController.js index c9e55d3..705ed32 100755 --- a/app/Game/Channel/Control/PlayerController.js +++ b/app/Game/Channel/Control/PlayerController.js @@ -1,24 +1,24 @@ define([ "Game/Core/Control/PlayerController", "Lib/Utilities/NotificationCenter", - "Lib/Utilities/Protocol/Parser", - "Game/Config/Settings" + "Lib/Utilities/Protocol/Parser" ], - -function(Parent, nc, Parser, Settings) { + +function(Parent, nc, Parser) { "use strict"; function PlayerController(player) { Parent.call(this, player); + this._lastProcessedSeq = 0; } PlayerController.prototype = Object.create(Parent.prototype); - /* - * retrieves move (and other) commands from client and executes them at the server - */ + /* + * retrieves move (and other) commands from client and executes them at the server + */ PlayerController.prototype.applyCommand = function(options) { // FIXME: remove this function and use ProtocolHelper.applyCommand() instead // Don't forget to change the function names to on... @@ -28,9 +28,16 @@ function(Parent, nc, Parser, Settings) { } else { message = options; } - + for (var command in message) { - this[command].call(this, message[command]); + var commandOptions = message[command]; + + // Track sequence number from client input commands + if (commandOptions && typeof commandOptions === 'object' && commandOptions._seq !== undefined) { + this._lastProcessedSeq = commandOptions._seq; + } + + this[command].call(this, commandOptions); } }; @@ -45,36 +52,6 @@ function(Parent, nc, Parser, Settings) { this.player.suicide(); }; - PlayerController.prototype.mePositionStateOverride = function(update) { - - if(!this.player.isSpawned()) { - // if someone still falls but is dead on the server already - return; - } - - var difference = { - x: Math.abs(update.p.x - this.player.doll.body.GetPosition().x), - y: Math.abs(update.p.y - this.player.doll.body.GetPosition().y) - }; - - if(difference.x < Settings.PUNKBUSTER_DIFFERENCE_METERS && - difference.y < Settings.PUNKBUSTER_DIFFERENCE_METERS) { - this.player.doll.updatePositionState(update); - } else { - // HARD UPDATE FOR SELF - console.log(this.player.user.options.nickname + " is cheating."); - - var body = this.player.doll.body; - - var options = { - p: body.GetPosition(), - lv: body.GetLinearVelocity() - }; - - nc.trigger(nc.ns.channel.to.client.user.gameCommand.send + this.player.id, "positionStateReset", options); - } - }; - return PlayerController; }); \ No newline at end of file diff --git a/app/Game/Channel/GameController.js b/app/Game/Channel/GameController.js index 4f5f5b5..a010d0b 100755 --- a/app/Game/Channel/GameController.js +++ b/app/Game/Channel/GameController.js @@ -113,13 +113,30 @@ function (Parent, PhysicsEngine, Settings, requestAnimFrame, nc, Box2D, Player, }; GameController.prototype.updateWorld = function () { - + var update = this.getWorldUpdateObject(false); if(Object.getOwnPropertyNames(update).length > 0) { nc.trigger(nc.ns.channel.to.client.gameCommand.broadcast, "worldUpdate", update); } + // Send per-user input acknowledgments for server reconciliation + for (var id in this.players) { + var player = this.players[id]; + if (player.isSpawned() && player.playerController._lastProcessedSeq > 0) { + var body = player.doll.body; + nc.trigger( + nc.ns.channel.to.client.user.gameCommand.send + id, + "inputAck", + { + seq: player.playerController._lastProcessedSeq, + p: { x: body.GetPosition().x, y: body.GetPosition().y }, + lv: { x: body.GetLinearVelocity().x, y: body.GetLinearVelocity().y } + } + ); + } + } + this.worldUpdateTimeout = setTimeout(this.updateWorld.bind(this), Settings.NETWORK_UPDATE_INTERVAL); }; diff --git a/app/Game/Channel/GameObjects/Doll.js b/app/Game/Channel/GameObjects/Doll.js index 572beac..04ddae5 100755 --- a/app/Game/Channel/GameObjects/Doll.js +++ b/app/Game/Channel/GameObjects/Doll.js @@ -2,11 +2,10 @@ define([ "Game/Core/GameObjects/Doll", "Game/Channel/GameObjects/Item", "Lib/Vendor/Box2D", - "Lib/Utilities/NotificationCenter", - "Lib/Utilities/Assert" + "Lib/Utilities/NotificationCenter" ], - -function (Parent, Item, Box2D, nc, Assert) { + +function (Parent, Item, Box2D, nc) { "use strict"; @@ -93,16 +92,6 @@ function (Parent, Item, Box2D, nc, Assert) { } }; - Doll.prototype.updatePositionState = function(update) { - if(!this.isAnotherPlayerNearby()) { - Assert.number(update.p.x, update.p.y); - Assert.number(update.lv.x, update.lv.y); - this.body.SetAwake(true); - this.body.SetPosition(update.p); - this.body.SetLinearVelocity(update.lv); - } - }; - Doll.prototype.getUpdateData = function(getSleeping) { var updateData = Parent.prototype.getUpdateData.call(this, getSleeping); diff --git a/app/Game/Client/Control/PlayerController.js b/app/Game/Client/Control/PlayerController.js index 82db175..c555682 100755 --- a/app/Game/Client/Control/PlayerController.js +++ b/app/Game/Client/Control/PlayerController.js @@ -14,6 +14,7 @@ function (Parent, nc, KeyboardAndMouse, Gamepad, pointerLockManager) { Parent.call(this, me); + this._inputSeq = 0; this.keyboardAndMouse = new KeyboardAndMouse(this); this.gamepad = new Gamepad(this); } @@ -26,34 +27,48 @@ function (Parent, nc, KeyboardAndMouse, Gamepad, pointerLockManager) { this.gamepad.update(); }; + PlayerController.prototype._recordAndSend = function(command) { + this._inputSeq++; + if (this.player.doll && this.player.doll.body) { + var vel = this.player.doll.body.GetLinearVelocity(); + this.player.inputBuffer.add({ + seq: this._inputSeq, + timestamp: Date.now(), + vx: vel.x, + vy: vel.y + }); + } + nc.trigger(nc.ns.client.to.server.gameCommand.send, command, {_seq: this._inputSeq}); + }; + PlayerController.prototype.moveLeft = function () { if (!this.isPlayerInputAllowed()) return; Parent.prototype.moveLeft.call(this); - nc.trigger(nc.ns.client.to.server.gameCommand.send, 'moveLeft'); + this._recordAndSend('moveLeft'); } PlayerController.prototype.moveRight = function () { if (!this.isPlayerInputAllowed()) return; Parent.prototype.moveRight.call(this); - nc.trigger(nc.ns.client.to.server.gameCommand.send, 'moveRight'); + this._recordAndSend('moveRight'); } // always allow to stop, to prevent endless running PlayerController.prototype.stop = function () { Parent.prototype.stop.call(this); - nc.trigger(nc.ns.client.to.server.gameCommand.send, 'stop'); + this._recordAndSend('stop'); } PlayerController.prototype.jump = function () { if (!this.isPlayerInputAllowed()) return; Parent.prototype.jump.call(this); - nc.trigger(nc.ns.client.to.server.gameCommand.send, 'jump'); + this._recordAndSend('jump'); } // always allow to stop. PlayerController.prototype.jumpStop = function () { Parent.prototype.jumpStop.call(this); - nc.trigger(nc.ns.client.to.server.gameCommand.send, 'jumpStop'); + this._recordAndSend('jumpStop'); } PlayerController.prototype.setXY = function(x, y) { diff --git a/app/Game/Client/GameController.js b/app/Game/Client/GameController.js index 9d85885..08c6c10 100755 --- a/app/Game/Client/GameController.js +++ b/app/Game/Client/GameController.js @@ -49,10 +49,9 @@ function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, reque this.animationRequestId = requestAnimFrame(this.update.bind(this)); this.physicsEngine.update(); - + if(this.me) { this.me.update(); - this.mePositionStateOverride(); } nc.trigger(nc.ns.client.game.events.render); @@ -61,12 +60,32 @@ function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, reque domController.fpsStep(); }; - GameController.prototype.mePositionStateOverride = function() { - if(this.me.isPositionStateOverrideNeeded()) { - nc.trigger( - nc.ns.client.to.server.gameCommand.send, - "mePositionStateOverride", - this.me.getPositionStateOverride() + GameController.prototype.onInputAck = function(ackData) { + if (!this.me || !this.me.doll) return; + + this.me.inputBuffer.acknowledgeUpTo(ackData.seq); + + var currentPos = this.me.doll.body.GetPosition(); + var diffX = Math.abs(ackData.p.x - currentPos.x); + var diffY = Math.abs(ackData.p.y - currentPos.y); + + // Scale the acceptable drift by how many inputs the server + // hasn't processed yet. More unacked time = more expected drift. + var unacked = this.me.inputBuffer.getUnacknowledged(); + var expectedDrift = 0; + if (unacked.length > 0) { + var unackedTime = (Date.now() - unacked[0].timestamp) / 1000; + expectedDrift = unackedTime * Settings.RUN_SPEED; + } + + var threshold = Settings.RECONCILIATION_THRESHOLD + expectedDrift; + + // Only correct when the error exceeds what latency can explain — + // meaning something unexpected happened (collision, being hit, etc.) + if (diffX > threshold || diffY > threshold) { + this.me.applyReconciliation( + ackData.p.x, ackData.p.y, + ackData.lv.x, ackData.lv.y ); } }; @@ -108,36 +127,15 @@ function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, reque }; - /* - - TODO : - - remove this - - overwrite setUpdateData inside client / Me with an empty function - - GameController.prototype.onWorldUpdateGameObject = function(body, gameObject, update) { - if(gameObject === this.me.doll) { - this.me.setLastServerPositionState(update); - if(!this.me.acceptPositionStateUpdateFromServer()) { - return; // this is to ignore own doll updates from world update - } - } - - Parent.prototype.onWorldUpdateGameObject.call(this, body, gameObject, update); - }; - */ - GameController.prototype.onRemoveGameObject = function(options) { }; + // Own doll position is handled by onInputAck reconciliation, not worldUpdate GameController.prototype.updateGameObject = function (gameObject, gameObjectUpdate) { - if(gameObject === this.me.doll) { - this.me.setLastServerPositionState(gameObjectUpdate); - if(!this.me.acceptPositionStateUpdateFromServer()) { - return; // this is to ignore own doll updates from world update - } - } - + if (gameObject === this.me.doll) { + return; + } Parent.prototype.updateGameObject.call(this, gameObject, gameObjectUpdate); } @@ -166,7 +164,11 @@ function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, reque var player = this.players[playerId]; player.spawn(x, y); - + + if (player === this.me) { + this.me.inputBuffer.clear(); + } + if(options.holdingItemUid) { this.onHandActionResponse({ itemUid: options.holdingItemUid, @@ -222,6 +224,10 @@ function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, reque var killedByPlayer = this.players[options.killedByPlayerId]; player.kill(killedByPlayer, options.ragDollId); + if (player === this.me) { + this.me.inputBuffer.clear(); + } + nc.trigger(nc.ns.client.view.gameStats.kill, { victim: { name: player.user.options.nickname, @@ -235,10 +241,6 @@ function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, reque }); }; - GameController.prototype.onPositionStateReset = function(options) { - this.me.resetPositionState(options); - }; - GameController.prototype.loadLevel = function (path) { Parent.prototype.loadLevel.call(this, path); }; @@ -257,6 +259,7 @@ function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, reque GameController.prototype.endRound = function() { this.me.setInBetweenRounds(true); + this.me.inputBuffer.clear(); this.toggleGameStats(true); }; diff --git a/app/Game/Client/InputBuffer.js b/app/Game/Client/InputBuffer.js new file mode 100644 index 0000000..63fd574 --- /dev/null +++ b/app/Game/Client/InputBuffer.js @@ -0,0 +1,37 @@ +define([ +], + +function () { + + "use strict"; + + var MAX_BUFFER_SIZE = 300; + + function InputBuffer() { + this._buffer = []; + } + + InputBuffer.prototype.add = function(entry) { + this._buffer.push(entry); + if (this._buffer.length > MAX_BUFFER_SIZE) { + this._buffer.shift(); + } + }; + + InputBuffer.prototype.acknowledgeUpTo = function(seq) { + while (this._buffer.length > 0 && this._buffer[0].seq <= seq) { + this._buffer.shift(); + } + }; + + InputBuffer.prototype.getUnacknowledged = function() { + return this._buffer; + }; + + InputBuffer.prototype.clear = function() { + this._buffer = []; + }; + + return InputBuffer; + +}); diff --git a/app/Game/Client/Me.js b/app/Game/Client/Me.js index a904c04..fb0cdda 100644 --- a/app/Game/Client/Me.js +++ b/app/Game/Client/Me.js @@ -4,9 +4,10 @@ define([ "Lib/Utilities/NotificationCenter", "Lib/Utilities/Assert", "Game/Client/Control/PlayerController", + "Game/Client/InputBuffer", ], - -function (Parent, Settings, nc, Assert, PlayerController) { + +function (Parent, Settings, nc, Assert, PlayerController, InputBuffer) { "use strict"; @@ -19,12 +20,7 @@ function (Parent, Settings, nc, Assert, PlayerController) { y: 0 }; - this.lastServerPositionState = { - p: { - x: 0, - y: 0 - } - }; + this.inputBuffer = new InputBuffer(); this.arrowMesh = null; this.createAndAddArrow(); @@ -49,50 +45,25 @@ function (Parent, Settings, nc, Assert, PlayerController) { }; }; - Me.prototype.setLastServerPositionState = function(update) { - this.lastServerPositionState = update; - }; + Me.prototype.applyReconciliation = function(x, y, vx, vy) { + var currentPos = this.doll.body.GetPosition(); + var diffX = x - currentPos.x; + var diffY = y - currentPos.y; + var distance = Math.sqrt(diffX * diffX + diffY * diffY); - // Checks if client should send out its position to server - Me.prototype.isPositionStateOverrideNeeded = function() { - - if(!this.doll) { - return false; + if (distance > Settings.RECONCILIATION_SNAP_THRESHOLD) { + // Large error — snap immediately (server-side teleport, respawn, etc.) + this.doll.body.SetPosition({x: x, y: y}); + } else { + // Small error — blend toward reconciled position + var factor = Settings.RECONCILIATION_BLEND_FACTOR; + this.doll.body.SetPosition({ + x: currentPos.x + diffX * factor, + y: currentPos.y + diffY * factor + }); } - if(this.doll.isAnotherPlayerNearby()) { - return false; - } - - var difference = { - x: Math.abs(this.lastServerPositionState.p.x - this.doll.body.GetPosition().x), - y: Math.abs(this.lastServerPositionState.p.y - this.doll.body.GetPosition().y) - }; - - if(difference.x > Settings.ME_STATE_MAX_DIFFERENCE_METERS || - difference.y > Settings.ME_STATE_MAX_DIFFERENCE_METERS) { - return true; - } - return false; - }; - - Me.prototype.getPositionStateOverride = function() { - return { - p: this.doll.body.GetPosition().Copy(), - lv: this.doll.body.GetLinearVelocity().Copy() - }; - }; - - Me.prototype.acceptPositionStateUpdateFromServer = function() { - // gamecontroller should accept me's doll update only when another players doll is nearby. - return this.doll.isAnotherPlayerNearby(); - }; - - Me.prototype.resetPositionState = function(options) { - Assert.number(options.p.x, options.p.y); - Assert.number(options.lv.x, options.lv.y); - this.doll.body.SetPosition(options.p); - this.doll.body.SetLinearVelocity(options.lv); + this.doll.body.SetLinearVelocity({x: vx, y: vy}); }; Me.prototype.createAndAddArrow = function() { diff --git a/app/Game/Config/Settings.js b/app/Game/Config/Settings.js index 10cf76f..7a69d5b 100755 --- a/app/Game/Config/Settings.js +++ b/app/Game/Config/Settings.js @@ -93,9 +93,10 @@ function () { CHANNEL_DEFAULT_LEVELS: ["debug"], CHANNEL_RECORD_SESSION: false, - // ME STATE - ME_STATE_MAX_DIFFERENCE_METERS: 1, - PUNKBUSTER_DIFFERENCE_METERS: 1 + // RECONCILIATION + RECONCILIATION_THRESHOLD: 1, + RECONCILIATION_SNAP_THRESHOLD: 5.0, + RECONCILIATION_BLEND_FACTOR: 0.2 }; Settings.TILE_RATIO = Settings.ORIGINAL_TILE_SIZE / Settings.TILE_SIZE;