mirror of
https://github.com/logsol/chuck.js.git
synced 2026-05-11 10:37:34 +00:00
The old PUNKBUSTER check compared client-reported position to server position and snapped the player back when latency made them diverge, which felt like getting teleported under any real network conditions. Replaces that with proper client-side prediction + reconciliation: client tags each input with a sequence number and keeps an input buffer; server tracks the last processed sequence and reports its authoritative position via a per-user inputAck alongside each worldUpdate. The client only corrects when the actual disagreement exceeds what the unacked input time can explain — so steady-state movement runs purely on local physics, and only genuine unexpected events (collisions, being hit) trigger a smooth blend toward the server state. Includes adaptive threshold scaling so high-latency sessions don't false-positive corrections during normal running.
284 lines
8.7 KiB
JavaScript
Executable file
284 lines
8.7 KiB
JavaScript
Executable file
define([
|
|
"Game/Core/GameController",
|
|
"Lib/Vendor/Box2D",
|
|
"Game/Client/Physics/Engine",
|
|
"Game/Client/View/ViewManager",
|
|
"Game/Client/Control/PlayerController",
|
|
"Lib/Utilities/NotificationCenter",
|
|
"Lib/Utilities/RequestAnimFrame",
|
|
"Game/Config/Settings",
|
|
"Game/Client/GameObjects/GameObject",
|
|
"Game/Client/GameObjects/Doll",
|
|
"Game/Client/View/DomController",
|
|
"Lib/Utilities/Protocol/Helper",
|
|
"Game/Client/Me",
|
|
"Game/Client/AudioPlayer",
|
|
"Game/Client/PointerLockManager",
|
|
"Lib/Utilities/Assert",
|
|
"Lib/Utilities/Exception"
|
|
],
|
|
|
|
function (Parent, Box2D, PhysicsEngine, ViewManager, PlayerController, nc, requestAnimFrame, Settings, GameObject, Doll, domController, ProtocolHelper, Me, AudioPlayer, pointerLockManager, Assert, Exception) {
|
|
|
|
"use strict";
|
|
|
|
function GameController (options) {
|
|
|
|
this.clientIsReady = false;
|
|
this.view = ViewManager.createView();
|
|
this.me = null;
|
|
this.animationRequestId = null;
|
|
this.audioPlayer = null;
|
|
|
|
Parent.call(this, options);
|
|
|
|
this.ncTokens = this.ncTokens.concat([
|
|
nc.on(nc.ns.client.game.gameStats.toggle, this.toggleGameStats, this)
|
|
]);
|
|
}
|
|
|
|
GameController.prototype = Object.create(Parent.prototype);
|
|
|
|
GameController.prototype.getMe = function () {
|
|
return this.me;
|
|
};
|
|
|
|
GameController.prototype.update = function () {
|
|
|
|
Parent.prototype.update.call(this);
|
|
|
|
this.animationRequestId = requestAnimFrame(this.update.bind(this));
|
|
this.physicsEngine.update();
|
|
|
|
if(this.me) {
|
|
this.me.update();
|
|
}
|
|
|
|
nc.trigger(nc.ns.client.game.events.render);
|
|
|
|
this.view.render();
|
|
domController.fpsStep();
|
|
};
|
|
|
|
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
|
|
);
|
|
}
|
|
};
|
|
|
|
GameController.prototype.onClientReadyResponse = function(options) {
|
|
var i;
|
|
|
|
if (options.runtimeItems) {
|
|
for (i = 0; i < options.runtimeItems.length; i++) {
|
|
|
|
var itemDef = options.runtimeItems[i];
|
|
|
|
if(!this.getItemByUid(itemDef.uid)) {
|
|
// When creating from synchronization we need to bring it into level format (px)
|
|
itemDef.options.x *= Settings.RATIO;
|
|
itemDef.options.y *= Settings.RATIO;
|
|
this.level.createItem(itemDef.uid, itemDef.options);
|
|
console.log("Creating runtime Item: ", itemDef.options.name, itemDef.uid)
|
|
}
|
|
}
|
|
}
|
|
|
|
this.setMe();
|
|
|
|
this.clientIsReady = true; // needs to stay before onSpawnPlayer
|
|
|
|
if (options.spawnedPlayers) {
|
|
for(i = 0; i < options.spawnedPlayers.length; i++) {
|
|
this.onSpawnPlayer(options.spawnedPlayers[i]);
|
|
}
|
|
}
|
|
|
|
if (options.worldUpdate) { // needs to stay after onSpawnPlayer otherwise others doll will not be there
|
|
this.onWorldUpdate(options.worldUpdate);
|
|
}
|
|
|
|
//this.audioPlayer = new AudioPlayer(Settings.AUDIO_PATH + "city.mp3");
|
|
//this.audioPlayer.play();
|
|
};
|
|
|
|
|
|
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) {
|
|
return;
|
|
}
|
|
Parent.prototype.updateGameObject.call(this, gameObject, gameObjectUpdate);
|
|
}
|
|
|
|
GameController.prototype.createMe = function(user) {
|
|
this.me = new Me(user.id, this.physicsEngine, user);
|
|
this.players[user.id] = this.me;
|
|
};
|
|
|
|
GameController.prototype.setMe = function() {
|
|
this.view.setMe(this.me);
|
|
};
|
|
|
|
GameController.prototype.onGameCommand = function(message) {
|
|
ProtocolHelper.applyCommand(message, this);
|
|
};
|
|
|
|
GameController.prototype.onSpawnPlayer = function(options) {
|
|
|
|
if(!this.clientIsReady) {
|
|
return;
|
|
}
|
|
|
|
var playerId = options.id,
|
|
x = options.x,
|
|
y = options.y;
|
|
|
|
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,
|
|
action: "grab",
|
|
playerId: playerId
|
|
});
|
|
}
|
|
};
|
|
|
|
GameController.prototype.onHandActionResponse = function(options) {
|
|
var player = this.players[options.playerId];
|
|
var item = this.getItemByUid(options.itemUid);
|
|
|
|
if(item) {
|
|
if(options.action == "throw") {
|
|
player.throw(options, item);
|
|
} else if(options.action == "grab") {
|
|
player.grab(item);
|
|
}
|
|
} else {
|
|
console.warn("Item for joint can not be found locally. " + options.itemUid);
|
|
}
|
|
|
|
};
|
|
|
|
GameController.prototype.onUpdateStats = function(options) {
|
|
var player = this.players[options.playerId];
|
|
if(!player) {
|
|
throw new Exception("No player with id: " + options.playerId);
|
|
}
|
|
player.setStats(options.stats);
|
|
|
|
var playersArray = [];
|
|
for (var key in this.players) {
|
|
playersArray.push(this.players[key]);
|
|
}
|
|
|
|
var sortedPlayers = playersArray.sort(function(a,b) {
|
|
if(a.stats.score > b.stats.score) return -1;
|
|
if(a.stats.score < b.stats.score) return 1;
|
|
if(a.stats.deaths < b.stats.deaths) return -1;
|
|
if(a.stats.deaths > b.stats.deaths) return 1;
|
|
if(a.stats.health > b.stats.health) return -1;
|
|
if(a.stats.health < b.stats.health) return 1;
|
|
return 0;
|
|
});
|
|
|
|
nc.trigger(nc.ns.client.view.gameStats.update, sortedPlayers);
|
|
};
|
|
|
|
GameController.prototype.onPlayerKill = function(options) {
|
|
var player = this.players[options.playerId];
|
|
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,
|
|
isMe: player === this.me
|
|
},
|
|
killer: {
|
|
name: killedByPlayer.user.options.nickname,
|
|
isMe: killedByPlayer === this.me
|
|
},
|
|
item: options.item
|
|
});
|
|
};
|
|
|
|
GameController.prototype.loadLevel = function (path) {
|
|
Parent.prototype.loadLevel.call(this, path);
|
|
};
|
|
|
|
GameController.prototype.onLevelLoaded = function () {
|
|
pointerLockManager.update(null, {start:true});
|
|
};
|
|
|
|
GameController.prototype.toggleGameStats = function(show) {
|
|
nc.trigger(nc.ns.client.view.gameStats.toggle, show);
|
|
};
|
|
|
|
GameController.prototype.beginRound = function() {
|
|
this.me.setInBetweenRounds(false);
|
|
};
|
|
|
|
GameController.prototype.endRound = function() {
|
|
this.me.setInBetweenRounds(true);
|
|
this.me.inputBuffer.clear();
|
|
this.toggleGameStats(true);
|
|
};
|
|
|
|
GameController.prototype.destroy = function() {
|
|
|
|
if (!window.cancelAnimationFrame) {
|
|
window.cancelAnimationFrame = function(id) {
|
|
clearTimeout(id);
|
|
};
|
|
}
|
|
|
|
cancelAnimationFrame(this.animationRequestId);
|
|
|
|
Parent.prototype.destroy.call(this);
|
|
|
|
//this.audioPlayer.destroy();
|
|
|
|
this.view.destroy();
|
|
};
|
|
|
|
return GameController;
|
|
});
|