diff --git a/memo.txt b/memo.txt index 30c217d1..4ce404f6 100644 --- a/memo.txt +++ b/memo.txt @@ -9,7 +9,8 @@ Done: - init de GameComponents partagé entre serveur et client. - draw on the canvas "WIN", "LOSE", "MATCHMAKING COMPLETE", ... - 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: - mode multi-balles diff --git a/src/client/class/InputHistory.ts b/src/client/class/InputHistory.ts index cb7e8644..e4d3b8f1 100644 --- a/src/client/class/InputHistory.ts +++ b/src/client/class/InputHistory.ts @@ -1,13 +1,14 @@ import * as en from "../../shared_js/enums.js" +import * as ev from "../../shared_js/class/Event.js" class InputHistory { input: en.InputEnum; - inputId: number; + id: number; deltaTime: number; - constructor(input: en.InputEnum, inputId: number, deltaTime: number) { - this.input = input; - this.inputId = inputId; + constructor(inputState: ev.EventInput, deltaTime: number) { + this.input = inputState.input; + this.id = inputState.id; this.deltaTime = deltaTime; } } diff --git a/src/client/constants.ts b/src/client/constants.ts index 940cfce2..5a3f4d9f 100644 --- a/src/client/constants.ts +++ b/src/client/constants.ts @@ -8,9 +8,12 @@ export const gridSize = Math.floor(w/500); // min interval on Firefox seems to be 15. Chrome can go lower. export const handleInputIntervalMS = 15; // millisecond +export const sendLoopIntervalMS = 15; // millisecond export const gameLoopIntervalMS = 15; // millisecond export const drawLoopIntervalMS = 15; // millisecond +export const fixedDeltaTime = gameLoopIntervalMS/1000; // second + export const soundMutedFlag = true; export const soundRobloxVolume = 0.3; // between 0 and 1 export const soundPongVolume = 0.3; // between 0 and 1 diff --git a/src/client/gameLoop.ts b/src/client/gameLoop.ts index 1b8d212d..224e3d0e 100644 --- a/src/client/gameLoop.ts +++ b/src/client/gameLoop.ts @@ -1,4 +1,5 @@ +import * as c from "./constants.js"; import { gc, clientInfo } from "./global.js"; let actual_time: number = Date.now(); @@ -7,9 +8,12 @@ let delta_time: number; function gameLoop() { - last_time = actual_time; + /* last_time = actual_time; 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 // console.log(`dir.y: ${clientInfo.opponent.dir.y}, pos.y: ${clientInfo.opponent.pos.y}, opponentNextPos.y: ${clientInfo.opponentNextPos.y}`); diff --git a/src/client/handleInput.ts b/src/client/handleInput.ts index 78b52ad2..164680e1 100644 --- a/src/client/handleInput.ts +++ b/src/client/handleInput.ts @@ -3,6 +3,7 @@ import { pong, gc, socket, clientInfo } from "./global.js" import * as ev from "../shared_js/class/Event.js" import * as en from "../shared_js/enums.js" import { InputHistory } from "./class/InputHistory.js" +import * as c from "./constants.js"; export let gridDisplay = false; @@ -10,58 +11,66 @@ let actual_time: number = Date.now(); let last_time: number; let delta_time: number; +const inputState: ev.EventInput = new ev.EventInput(); const inputHistoryArr: InputHistory[] = []; -let id = 0; -/* idMax should be high enough to prevent duplicate "id" in "inputHistoryArr". -In theory a little more than (1000/handleInputIntervalMS) should be enough. */ -const idMax = 999; // 999 arbitrary + +// test +/* export function sendLoop() +{ + socket.send(JSON.stringify(inputState)); +} */ function handleInput() { - last_time = actual_time; + /* last_time = actual_time; 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; - if (keys.length == 0) { - return; - } - // console.log("handleInput"); - if (id > idMax) { - id = 0; - } - - if (keys.indexOf("g") != -1) + if (keys.length !== 0) { - gridDisplay = !gridDisplay; - pong.deleteKey("g"); + if (keys.indexOf("g") != -1) + { + 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) { - const input = new ev.EventInput(en.InputEnum.up, ++id); - inputHistoryArr.push(new InputHistory(input.input, input.inputId, delta)); - socket.send(JSON.stringify(input)); - playerMovePrediction(delta, input.input); // client prediction + inputState.input = en.InputEnum.up; } } else if (keys.indexOf("s") !== -1 || keys.indexOf("ArrowDown".toLowerCase()) !== -1) { - const input = new ev.EventInput(en.InputEnum.down, ++id); - inputHistoryArr.push(new InputHistory(input.input, input.inputId, delta)); - socket.send(JSON.stringify(input)); - playerMovePrediction(delta, input.input); // client prediction + inputState.input = en.InputEnum.down; } } -function testInputDelay(input: ev.EventInput) { - socket.send(JSON.stringify(input)); +function testInputDelay() { + socket.send(JSON.stringify(inputState)); } -// setTimeout(testInputDelay, 100, input); function playerMovePrediction(delta: number, input: en.InputEnum) @@ -81,16 +90,20 @@ function repeatInput(lastInputId: number) { // server reconciliation let i = inputHistoryArr.findIndex((value: InputHistory) => { - if (value.inputId === lastInputId) { + if (value.id === lastInputId) { return true; } return false; }); + // console.log(`inputHistory total: ${inputHistoryArr.length}` ); inputHistoryArr.splice(0, i+1); + // console.log(`inputHistory left: ${inputHistoryArr.length}` ); inputHistoryArr.forEach((value: InputHistory) => { - playerMovePrediction(value.deltaTime, value.input); + if (value.input !== en.InputEnum.noInput) { + playerMovePrediction(value.deltaTime, value.input); + } }); } diff --git a/src/client/pong.ts b/src/client/pong.ts index a6606704..05be7494 100644 --- a/src/client/pong.ts +++ b/src/client/pong.ts @@ -3,6 +3,7 @@ import * as c from "./constants.js" import { GameArea } from "./class/GameArea.js"; import { GameComponentsClient } from "./class/GameComponentsClient.js"; import { handleInput } from "./handleInput.js"; +// import { sendLoop } from "./handleInput.js"; import { gameLoop } from "./gameLoop.js" import { drawLoop } from "./draw.js"; import { countdown } from "./utils.js"; @@ -54,8 +55,9 @@ function resumeGame() pong.deleteKey(e.key); }); 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(drawLoop, c.drawLoopIntervalMS); + pong.drawLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS); } diff --git a/src/client/ws.ts b/src/client/ws.ts index ae4cc2e3..e80c640f 100644 --- a/src/client/ws.ts +++ b/src/client/ws.ts @@ -93,6 +93,9 @@ function gameUpdate(data: ev.EventGameUpdate) gc.ball.pos.assign(data.ball.x, data.ball.y); gc.ball.dir.assign(data.ball.dirX, data.ball.dirY); 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) { clientInfo.racket.pos.assign(clientInfo.racket.pos.x, data.playerLeft.y); } @@ -120,11 +123,23 @@ function gameUpdate(data: ev.EventGameUpdate) // server reconciliation 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) { - console.log("scoreUpdate"); + // console.log("scoreUpdate"); if (clientInfo.side === en.PlayerSide.left && data.scoreRight > gc.scoreRight.value) { soundRoblox.play(); } diff --git a/src/server/class/Client.ts b/src/server/class/Client.ts index ac9a8983..c1678848 100644 --- a/src/server/class/Client.ts +++ b/src/server/class/Client.ts @@ -10,7 +10,7 @@ class Client { isAlive: boolean = true; gameSession: GameSession; - inputBuffer: ev.EventInput; + inputBuffer: ev.EventInput = new ev.EventInput(); lastInputId: number = 0; constructor(socket: WebSocket, id: string) { diff --git a/src/server/class/GameSession.ts b/src/server/class/GameSession.ts index 866872d9..dc72e507 100644 --- a/src/server/class/GameSession.ts +++ b/src/server/class/GameSession.ts @@ -39,7 +39,7 @@ class GameSession { }); 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); } pause(s: GameSession) { @@ -50,7 +50,11 @@ class GameSession { clearInterval(s.gameLoopInterval); clearInterval(s.clientsUpdateInterval); } + instantInputDebug(client: ClientPlayer) { + this._handleInput(c.fixedDeltaTime, client); + } private _handleInput(delta: number, client: ClientPlayer) { + // if (client.inputBuffer === null) {return;} const gc = this.components; const input = client.inputBuffer.input; @@ -60,10 +64,13 @@ class GameSession { else if (input === en.InputEnum.down) { 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.inputBuffer = null; + client.lastInputId = client.inputBuffer.id; + // client.inputBuffer = null; } private _gameLoop(s: GameSession) { /* 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 = c.fixedDeltaTime; - s.playersMap.forEach( (client) => { - if (client.inputBuffer) { - s._handleInput(s.delta_time, client); - } - }); + // WIP, replaced by instantInputDebug() to prevent desynchro + /* s.playersMap.forEach( (client) => { + s._handleInput(s.delta_time, client); + }); */ const gc = s.components; if (gc.ballInPlay) diff --git a/src/server/constants.ts b/src/server/constants.ts index 761622fb..bd5f8e9e 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -2,8 +2,8 @@ export * from "../shared_js/constants.js" // 15ms == 1000/66.666 -export const gameLoopIntervalMS = 15; // millisecond -export const fixedDeltaTime = gameLoopIntervalMS/1000; // second +export const serverGameLoopIntervalMS = 15; // millisecond +export const fixedDeltaTime = serverGameLoopIntervalMS/1000; // second // 33.333ms == 1000/30 export const clientsUpdateIntervalMS = 1000/30; // millisecond diff --git a/src/server/wsServer.ts b/src/server/wsServer.ts index f8138c39..51c33c9b 100644 --- a/src/server/wsServer.ts +++ b/src/server/wsServer.ts @@ -58,8 +58,8 @@ function clientAnnounceListener(this: WebSocket, data: string) try { const msg : ev.ClientAnnounce = JSON.parse(data); if (msg.type === en.EventTypes.clientAnnounce) { - // TODO: reconnection with msg.id ? - // TODO: spectator/player distinction with msg.type ? + // TODO: reconnection with msg.clientId ? + // TODO: spectator/player distinction with msg.role ? this.send(JSON.stringify( new ev.EventAssignId(this.id) )); 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) { const client = clientsMap.get(this.id) as ClientPlayer; client.inputBuffer = input; + client.gameSession.instantInputDebug(client); // wip } else { console.log("Invalid clientInput"); diff --git a/src/shared_js/class/Event.ts b/src/shared_js/class/Event.ts index d35d1f0d..14b86ff3 100644 --- a/src/shared_js/class/Event.ts +++ b/src/shared_js/class/Event.ts @@ -66,7 +66,7 @@ class EventMatchEnd extends ServerEvent { /* From Client */ class ClientEvent { - type: en.EventTypes; + type: en.EventTypes; // readonly ? constructor(type: en.EventTypes = 0) { this.type = type; } @@ -74,21 +74,21 @@ class ClientEvent { class ClientAnnounce extends ClientEvent { role: en.ClientRole; - id: string; - constructor(role: en.ClientRole, id: string = "") { + clientId: string; + constructor(role: en.ClientRole, clientId: string = "") { super(en.EventTypes.clientAnnounce); this.role = role; - this.id = id; + this.clientId = clientId; } } class EventInput extends ClientEvent { input: en.InputEnum; - inputId: number; - constructor(input: en.InputEnum = 0, inputId: number = 0) { + id: number; + constructor(input: en.InputEnum = en.InputEnum.noInput, id: number = 0) { super(en.EventTypes.clientInput); this.input = input; - this.inputId = inputId; + this.id = id; } } diff --git a/src/shared_js/class/Rectangle.ts b/src/shared_js/class/Rectangle.ts index 639dbd75..8b9e1260 100644 --- a/src/shared_js/class/Rectangle.ts +++ b/src/shared_js/class/Rectangle.ts @@ -44,10 +44,15 @@ class MovingRectangle extends Rectangle implements Moving { } move(delta: number) { // Math.floor WIP until VectorInteger debug // 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.y += Math.floor(this.dir.y * 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.x += this.dir.x * this.speed * delta; + this.pos.y += this.dir.y * this.speed * delta; } 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); this.move(delta); if (colliderArr.some(this.collision, this)) { @@ -60,6 +65,11 @@ class Racket extends MovingRectangle { constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { 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 { diff --git a/src/shared_js/enums.ts b/src/shared_js/enums.ts index 1acf2219..cc8ec311 100644 --- a/src/shared_js/enums.ts +++ b/src/shared_js/enums.ts @@ -22,8 +22,9 @@ enum EventTypes { } enum InputEnum { + noInput = 0, up = 1, - down + down, } enum PlayerSide {