input desynchro, rollback to instant handle.

This commit is contained in:
LuckyLaszlo
2022-11-30 04:00:55 +01:00
parent 01023d67b5
commit 68e529fec2
14 changed files with 123 additions and 66 deletions

View File

@@ -9,7 +9,8 @@ Done:
- init de GameComponents partagé entre serveur et client. - init de GameComponents partagé entre serveur et client.
- draw on the canvas "WIN", "LOSE", "MATCHMAKING COMPLETE", ... - draw on the canvas "WIN", "LOSE", "MATCHMAKING COMPLETE", ...
- interpolation (mis à jour progressif des mouvements de l'adversaire) - interpolation (mis à jour progressif des mouvements de l'adversaire)
- traitement groupé des inputs clients toutes les x millisecondes - traitement groupé des inputs clients toutes les x millisecondes
(BUG désynchronisation: revenu à un traitement immédiat en attendant)
TODO: TODO:
- mode multi-balles - mode multi-balles

View File

@@ -1,13 +1,14 @@
import * as en from "../../shared_js/enums.js" import * as en from "../../shared_js/enums.js"
import * as ev from "../../shared_js/class/Event.js"
class InputHistory { class InputHistory {
input: en.InputEnum; input: en.InputEnum;
inputId: number; id: number;
deltaTime: number; deltaTime: number;
constructor(input: en.InputEnum, inputId: number, deltaTime: number) { constructor(inputState: ev.EventInput, deltaTime: number) {
this.input = input; this.input = inputState.input;
this.inputId = inputId; this.id = inputState.id;
this.deltaTime = deltaTime; this.deltaTime = deltaTime;
} }
} }

View File

@@ -8,9 +8,12 @@ export const gridSize = Math.floor(w/500);
// min interval on Firefox seems to be 15. Chrome can go lower. // min interval on Firefox seems to be 15. Chrome can go lower.
export const handleInputIntervalMS = 15; // millisecond export const handleInputIntervalMS = 15; // millisecond
export const sendLoopIntervalMS = 15; // millisecond
export const gameLoopIntervalMS = 15; // millisecond export const gameLoopIntervalMS = 15; // millisecond
export const drawLoopIntervalMS = 15; // millisecond export const drawLoopIntervalMS = 15; // millisecond
export const fixedDeltaTime = gameLoopIntervalMS/1000; // second
export const soundMutedFlag = true; export const soundMutedFlag = true;
export const soundRobloxVolume = 0.3; // between 0 and 1 export const soundRobloxVolume = 0.3; // between 0 and 1
export const soundPongVolume = 0.3; // between 0 and 1 export const soundPongVolume = 0.3; // between 0 and 1

View File

@@ -1,4 +1,5 @@
import * as c from "./constants.js";
import { gc, clientInfo } from "./global.js"; import { gc, clientInfo } from "./global.js";
let actual_time: number = Date.now(); let actual_time: number = Date.now();
@@ -7,9 +8,12 @@ let delta_time: number;
function gameLoop() function gameLoop()
{ {
last_time = actual_time; /* last_time = actual_time;
actual_time = Date.now(); actual_time = Date.now();
delta_time = (actual_time - last_time) / 1000; delta_time = (actual_time - last_time) / 1000; */
delta_time = c.fixedDeltaTime;
// console.log(`delta_gameLoop: ${delta_time}`);
// interpolation // interpolation
// console.log(`dir.y: ${clientInfo.opponent.dir.y}, pos.y: ${clientInfo.opponent.pos.y}, opponentNextPos.y: ${clientInfo.opponentNextPos.y}`); // console.log(`dir.y: ${clientInfo.opponent.dir.y}, pos.y: ${clientInfo.opponent.pos.y}, opponentNextPos.y: ${clientInfo.opponentNextPos.y}`);

View File

@@ -3,6 +3,7 @@ import { pong, gc, socket, clientInfo } from "./global.js"
import * as ev from "../shared_js/class/Event.js" import * as ev from "../shared_js/class/Event.js"
import * as en from "../shared_js/enums.js" import * as en from "../shared_js/enums.js"
import { InputHistory } from "./class/InputHistory.js" import { InputHistory } from "./class/InputHistory.js"
import * as c from "./constants.js";
export let gridDisplay = false; export let gridDisplay = false;
@@ -10,58 +11,66 @@ let actual_time: number = Date.now();
let last_time: number; let last_time: number;
let delta_time: number; let delta_time: number;
const inputState: ev.EventInput = new ev.EventInput();
const inputHistoryArr: InputHistory[] = []; const inputHistoryArr: InputHistory[] = [];
let id = 0;
/* idMax should be high enough to prevent duplicate "id" in "inputHistoryArr". // test
In theory a little more than (1000/handleInputIntervalMS) should be enough. */ /* export function sendLoop()
const idMax = 999; // 999 arbitrary {
socket.send(JSON.stringify(inputState));
} */
function handleInput() function handleInput()
{ {
last_time = actual_time; /* last_time = actual_time;
actual_time = Date.now(); actual_time = Date.now();
delta_time = (actual_time - last_time) / 1000; delta_time = (actual_time - last_time) / 1000; */
delta_time = c.fixedDeltaTime;
// console.log(`delta_time: ${delta_time}`);
inputState.id = Date.now();
inputState.input = en.InputEnum.noInput;
const keys = pong.keys; const keys = pong.keys;
if (keys.length == 0) { if (keys.length !== 0)
return;
}
// console.log("handleInput");
if (id > idMax) {
id = 0;
}
if (keys.indexOf("g") != -1)
{ {
gridDisplay = !gridDisplay; if (keys.indexOf("g") != -1)
pong.deleteKey("g"); {
gridDisplay = !gridDisplay;
pong.deleteKey("g");
}
playerMovements(delta_time, keys);
} }
playerMove(delta_time, keys); socket.send(JSON.stringify(inputState));
// setTimeout(testInputDelay, 100);
inputHistoryArr.push(new InputHistory(inputState, delta_time));
// client prediction
if (inputState.input !== en.InputEnum.noInput) {
// TODO: peut-etre le mettre dans game loop ?
// Attention au delta time dans ce cas !
playerMovePrediction(delta_time, inputState.input);
}
} }
function playerMove(delta: number, keys: string[]) function playerMovements(delta: number, keys: string[])
{ {
if (keys.indexOf("w") !== -1 || keys.indexOf("ArrowUp".toLowerCase()) !== -1) { if (keys.indexOf("w") !== -1 || keys.indexOf("ArrowUp".toLowerCase()) !== -1)
{
if (keys.indexOf("s") === -1 && keys.indexOf("ArrowDown".toLowerCase()) === -1) { if (keys.indexOf("s") === -1 && keys.indexOf("ArrowDown".toLowerCase()) === -1) {
const input = new ev.EventInput(en.InputEnum.up, ++id); inputState.input = en.InputEnum.up;
inputHistoryArr.push(new InputHistory(input.input, input.inputId, delta));
socket.send(JSON.stringify(input));
playerMovePrediction(delta, input.input); // client prediction
} }
} }
else if (keys.indexOf("s") !== -1 || keys.indexOf("ArrowDown".toLowerCase()) !== -1) { else if (keys.indexOf("s") !== -1 || keys.indexOf("ArrowDown".toLowerCase()) !== -1) {
const input = new ev.EventInput(en.InputEnum.down, ++id); inputState.input = en.InputEnum.down;
inputHistoryArr.push(new InputHistory(input.input, input.inputId, delta));
socket.send(JSON.stringify(input));
playerMovePrediction(delta, input.input); // client prediction
} }
} }
function testInputDelay(input: ev.EventInput) { function testInputDelay() {
socket.send(JSON.stringify(input)); socket.send(JSON.stringify(inputState));
} }
// setTimeout(testInputDelay, 100, input);
function playerMovePrediction(delta: number, input: en.InputEnum) function playerMovePrediction(delta: number, input: en.InputEnum)
@@ -81,16 +90,20 @@ function repeatInput(lastInputId: number)
{ {
// server reconciliation // server reconciliation
let i = inputHistoryArr.findIndex((value: InputHistory) => { let i = inputHistoryArr.findIndex((value: InputHistory) => {
if (value.inputId === lastInputId) { if (value.id === lastInputId) {
return true; return true;
} }
return false; return false;
}); });
// console.log(`inputHistory total: ${inputHistoryArr.length}` );
inputHistoryArr.splice(0, i+1); inputHistoryArr.splice(0, i+1);
// console.log(`inputHistory left: ${inputHistoryArr.length}` );
inputHistoryArr.forEach((value: InputHistory) => { inputHistoryArr.forEach((value: InputHistory) => {
playerMovePrediction(value.deltaTime, value.input); if (value.input !== en.InputEnum.noInput) {
playerMovePrediction(value.deltaTime, value.input);
}
}); });
} }

View File

@@ -3,6 +3,7 @@ import * as c from "./constants.js"
import { GameArea } from "./class/GameArea.js"; import { GameArea } from "./class/GameArea.js";
import { GameComponentsClient } from "./class/GameComponentsClient.js"; import { GameComponentsClient } from "./class/GameComponentsClient.js";
import { handleInput } from "./handleInput.js"; import { handleInput } from "./handleInput.js";
// import { sendLoop } from "./handleInput.js";
import { gameLoop } from "./gameLoop.js" import { gameLoop } from "./gameLoop.js"
import { drawLoop } from "./draw.js"; import { drawLoop } from "./draw.js";
import { countdown } from "./utils.js"; import { countdown } from "./utils.js";
@@ -54,8 +55,9 @@ function resumeGame()
pong.deleteKey(e.key); pong.deleteKey(e.key);
}); });
pong.handleInputInterval = window.setInterval(handleInput, c.handleInputIntervalMS); pong.handleInputInterval = window.setInterval(handleInput, c.handleInputIntervalMS);
// pong.handleInputInterval = window.setInterval(sendLoop, c.sendLoopIntervalMS);
pong.gameLoopInterval = window.setInterval(gameLoop, c.gameLoopIntervalMS); pong.gameLoopInterval = window.setInterval(gameLoop, c.gameLoopIntervalMS);
pong.gameLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS); pong.drawLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS);
} }

View File

@@ -93,6 +93,9 @@ function gameUpdate(data: ev.EventGameUpdate)
gc.ball.pos.assign(data.ball.x, data.ball.y); gc.ball.pos.assign(data.ball.x, data.ball.y);
gc.ball.dir.assign(data.ball.dirX, data.ball.dirY); gc.ball.dir.assign(data.ball.dirX, data.ball.dirY);
gc.ball.speed = data.ball.speed; gc.ball.speed = data.ball.speed;
const predictionPos = new VectorInteger(clientInfo.racket.pos.x, clientInfo.racket.pos.y); // debug
if (clientInfo.side === en.PlayerSide.left) { if (clientInfo.side === en.PlayerSide.left) {
clientInfo.racket.pos.assign(clientInfo.racket.pos.x, data.playerLeft.y); clientInfo.racket.pos.assign(clientInfo.racket.pos.x, data.playerLeft.y);
} }
@@ -120,11 +123,23 @@ function gameUpdate(data: ev.EventGameUpdate)
// server reconciliation // server reconciliation
repeatInput(data.lastInputId); repeatInput(data.lastInputId);
// debug
if (clientInfo.racket.pos.y > predictionPos.y + 1
|| clientInfo.racket.pos.y < predictionPos.y - 1)
{
console.log(
`Reconciliation error:
server y: ${data.playerLeft.y}
reconciliation y: ${clientInfo.racket.pos.y}
prediction y: ${predictionPos.y}`
);
}
} }
function scoreUpdate(data: ev.EventScoreUpdate) function scoreUpdate(data: ev.EventScoreUpdate)
{ {
console.log("scoreUpdate"); // console.log("scoreUpdate");
if (clientInfo.side === en.PlayerSide.left && data.scoreRight > gc.scoreRight.value) { if (clientInfo.side === en.PlayerSide.left && data.scoreRight > gc.scoreRight.value) {
soundRoblox.play(); soundRoblox.play();
} }

View File

@@ -10,7 +10,7 @@ class Client {
isAlive: boolean = true; isAlive: boolean = true;
gameSession: GameSession; gameSession: GameSession;
inputBuffer: ev.EventInput; inputBuffer: ev.EventInput = new ev.EventInput();
lastInputId: number = 0; lastInputId: number = 0;
constructor(socket: WebSocket, id: string) { constructor(socket: WebSocket, id: string) {

View File

@@ -39,7 +39,7 @@ class GameSession {
}); });
s.actual_time = Date.now(); s.actual_time = Date.now();
s.gameLoopInterval = setInterval(s._gameLoop, c.gameLoopIntervalMS, s); s.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s);
s.clientsUpdateInterval = setInterval(s._clientsUpdate, c.clientsUpdateIntervalMS, s); s.clientsUpdateInterval = setInterval(s._clientsUpdate, c.clientsUpdateIntervalMS, s);
} }
pause(s: GameSession) { pause(s: GameSession) {
@@ -50,7 +50,11 @@ class GameSession {
clearInterval(s.gameLoopInterval); clearInterval(s.gameLoopInterval);
clearInterval(s.clientsUpdateInterval); clearInterval(s.clientsUpdateInterval);
} }
instantInputDebug(client: ClientPlayer) {
this._handleInput(c.fixedDeltaTime, client);
}
private _handleInput(delta: number, client: ClientPlayer) { private _handleInput(delta: number, client: ClientPlayer) {
// if (client.inputBuffer === null) {return;}
const gc = this.components; const gc = this.components;
const input = client.inputBuffer.input; const input = client.inputBuffer.input;
@@ -60,10 +64,13 @@ class GameSession {
else if (input === en.InputEnum.down) { else if (input === en.InputEnum.down) {
client.racket.dir.y = 1; client.racket.dir.y = 1;
} }
client.racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
if (input !== en.InputEnum.noInput) {
client.racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
}
client.lastInputId = client.inputBuffer.inputId; client.lastInputId = client.inputBuffer.id;
client.inputBuffer = null; // client.inputBuffer = null;
} }
private _gameLoop(s: GameSession) { private _gameLoop(s: GameSession) {
/* s.last_time = s.actual_time; /* s.last_time = s.actual_time;
@@ -71,11 +78,10 @@ class GameSession {
s.delta_time = (s.actual_time - s.last_time) / 1000; */ s.delta_time = (s.actual_time - s.last_time) / 1000; */
s.delta_time = c.fixedDeltaTime; s.delta_time = c.fixedDeltaTime;
s.playersMap.forEach( (client) => { // WIP, replaced by instantInputDebug() to prevent desynchro
if (client.inputBuffer) { /* s.playersMap.forEach( (client) => {
s._handleInput(s.delta_time, client); s._handleInput(s.delta_time, client);
} }); */
});
const gc = s.components; const gc = s.components;
if (gc.ballInPlay) if (gc.ballInPlay)

View File

@@ -2,8 +2,8 @@
export * from "../shared_js/constants.js" export * from "../shared_js/constants.js"
// 15ms == 1000/66.666 // 15ms == 1000/66.666
export const gameLoopIntervalMS = 15; // millisecond export const serverGameLoopIntervalMS = 15; // millisecond
export const fixedDeltaTime = gameLoopIntervalMS/1000; // second export const fixedDeltaTime = serverGameLoopIntervalMS/1000; // second
// 33.333ms == 1000/30 // 33.333ms == 1000/30
export const clientsUpdateIntervalMS = 1000/30; // millisecond export const clientsUpdateIntervalMS = 1000/30; // millisecond

View File

@@ -58,8 +58,8 @@ function clientAnnounceListener(this: WebSocket, data: string)
try { try {
const msg : ev.ClientAnnounce = JSON.parse(data); const msg : ev.ClientAnnounce = JSON.parse(data);
if (msg.type === en.EventTypes.clientAnnounce) { if (msg.type === en.EventTypes.clientAnnounce) {
// TODO: reconnection with msg.id ? // TODO: reconnection with msg.clientId ?
// TODO: spectator/player distinction with msg.type ? // TODO: spectator/player distinction with msg.role ?
this.send(JSON.stringify( new ev.EventAssignId(this.id) )); this.send(JSON.stringify( new ev.EventAssignId(this.id) ));
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) )); this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) ));
@@ -152,6 +152,7 @@ export function clientInputListener(this: WebSocket, data: string)
if (input.type === en.EventTypes.clientInput) { if (input.type === en.EventTypes.clientInput) {
const client = clientsMap.get(this.id) as ClientPlayer; const client = clientsMap.get(this.id) as ClientPlayer;
client.inputBuffer = input; client.inputBuffer = input;
client.gameSession.instantInputDebug(client); // wip
} }
else { else {
console.log("Invalid clientInput"); console.log("Invalid clientInput");

View File

@@ -66,7 +66,7 @@ class EventMatchEnd extends ServerEvent {
/* From Client */ /* From Client */
class ClientEvent { class ClientEvent {
type: en.EventTypes; type: en.EventTypes; // readonly ?
constructor(type: en.EventTypes = 0) { constructor(type: en.EventTypes = 0) {
this.type = type; this.type = type;
} }
@@ -74,21 +74,21 @@ class ClientEvent {
class ClientAnnounce extends ClientEvent { class ClientAnnounce extends ClientEvent {
role: en.ClientRole; role: en.ClientRole;
id: string; clientId: string;
constructor(role: en.ClientRole, id: string = "") { constructor(role: en.ClientRole, clientId: string = "") {
super(en.EventTypes.clientAnnounce); super(en.EventTypes.clientAnnounce);
this.role = role; this.role = role;
this.id = id; this.clientId = clientId;
} }
} }
class EventInput extends ClientEvent { class EventInput extends ClientEvent {
input: en.InputEnum; input: en.InputEnum;
inputId: number; id: number;
constructor(input: en.InputEnum = 0, inputId: number = 0) { constructor(input: en.InputEnum = en.InputEnum.noInput, id: number = 0) {
super(en.EventTypes.clientInput); super(en.EventTypes.clientInput);
this.input = input; this.input = input;
this.inputId = inputId; this.id = id;
} }
} }

View File

@@ -44,10 +44,15 @@ class MovingRectangle extends Rectangle implements Moving {
} }
move(delta: number) { // Math.floor WIP until VectorInteger debug move(delta: number) { // Math.floor WIP until VectorInteger debug
// console.log(`delta: ${delta}, speed: ${this.speed}, speed*delta: ${this.speed * delta}`); // console.log(`delta: ${delta}, speed: ${this.speed}, speed*delta: ${this.speed * delta}`);
this.pos.x += Math.floor(this.dir.x * this.speed * delta); // this.pos.x += Math.floor(this.dir.x * this.speed * delta);
this.pos.y += Math.floor(this.dir.y * this.speed * delta); // this.pos.y += Math.floor(this.dir.y * this.speed * delta);
this.pos.x += this.dir.x * this.speed * delta;
this.pos.y += this.dir.y * this.speed * delta;
} }
moveAndCollide(delta: number, colliderArr: Rectangle[]) { moveAndCollide(delta: number, colliderArr: Rectangle[]) {
this._moveAndCollideAlgo(delta, colliderArr);
}
protected _moveAndCollideAlgo(delta: number, colliderArr: Rectangle[]) {
let oldPos = new VectorInteger(this.pos.x, this.pos.y); let oldPos = new VectorInteger(this.pos.x, this.pos.y);
this.move(delta); this.move(delta);
if (colliderArr.some(this.collision, this)) { if (colliderArr.some(this.collision, this)) {
@@ -60,6 +65,11 @@ class Racket extends MovingRectangle {
constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) {
super(pos, width, height, baseSpeed); super(pos, width, height, baseSpeed);
} }
moveAndCollide(delta: number, colliderArr: Rectangle[]) {
// let oldPos = new VectorInteger(this.pos.x, this.pos.y); // debug
this._moveAndCollideAlgo(delta, colliderArr);
// console.log(`y change: ${this.pos.y - oldPos.y}`);
}
} }
class Ball extends MovingRectangle { class Ball extends MovingRectangle {

View File

@@ -22,8 +22,9 @@ enum EventTypes {
} }
enum InputEnum { enum InputEnum {
noInput = 0,
up = 1, up = 1,
down down,
} }
enum PlayerSide { enum PlayerSide {