diff --git a/src/client/class/GameArea.ts b/src/client/class/GameArea.ts index 6831e8d6..96e1bfc8 100644 --- a/src/client/class/GameArea.ts +++ b/src/client/class/GameArea.ts @@ -2,12 +2,12 @@ import * as c from ".././constants.js" class GameArea { - keys: string[]; - interval: number = 0; + keys: string[] = []; + handleInputInterval: number = 0; + gameLoopInterval: number = 0; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; constructor() { - this.keys = []; this.canvas = document.createElement("canvas"); this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D; this.canvas.width = c.CanvasWidth; diff --git a/src/client/constants.ts b/src/client/constants.ts index c724e89a..ae6ecb66 100644 --- a/src/client/constants.ts +++ b/src/client/constants.ts @@ -7,6 +7,8 @@ export const scoreSize = Math.floor(w/16); 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 gameLoopIntervalMS = 15; // millisecond -export const soundRobloxVolume = 0.3; // between 0 and 1 -export const soundPongVolume = 0.3; // between 0 and 1 + +export const soundRobloxVolume = 0; // between 0 and 1 +export const soundPongVolume = 0; // between 0 and 1 diff --git a/src/client/gameLoop.ts b/src/client/gameLoop.ts index 04d4d846..4e069d5f 100644 --- a/src/client/gameLoop.ts +++ b/src/client/gameLoop.ts @@ -1,6 +1,5 @@ import {gc} from "./global.js"; import * as d from "./draw.js"; -import {handleInput} from "./handleInput.js"; let actual_time: number = Date.now(); let last_time: number; @@ -17,9 +16,7 @@ function gameLoop() actual_time = Date.now(); delta_time = (actual_time - last_time) / 1000; - handleInput(delta_time); - - // prediction + // client prediction gc.ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]); d.draw(); diff --git a/src/client/handleInput.ts b/src/client/handleInput.ts index 2e929042..0bee1aef 100644 --- a/src/client/handleInput.ts +++ b/src/client/handleInput.ts @@ -1,13 +1,42 @@ import {pong, gc, clientInfo} from "./global.js" import * as d from "./draw.js"; import { socket } from "./ws.js"; -import {InputEnum} from "../shared_js/enums.js" -import {EventInput} from "../shared_js/class/Event.js" +import * as ev from "../shared_js/class/Event.js" +import * as en from "../shared_js/enums.js" export let gridDisplay = false; -function handleInput(delta: number) +let actual_time: number = Date.now(); +let last_time: number; +let delta_time: number; + +class InputHistory { + input: en.InputEnum; + inputId: number; + deltaTime: number; + constructor(input: en.InputEnum, inputId: number, deltaTime: number) { + this.input = input; + this.inputId = inputId; + this.deltaTime = deltaTime; + } +} + +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 + +function handleInput() { + console.log("handleInput"); + last_time = actual_time; + actual_time = Date.now(); + delta_time = (actual_time - last_time) / 1000; + if (id > idMax) { + id = 0; + } + var keys = pong.keys; if (keys.length == 0) return; @@ -22,28 +51,63 @@ function handleInput(delta: number) gridDisplay = !gridDisplay; pong.deleteKey("g"); } - playerMove(delta, keys); + playerMove(delta_time, keys); } function playerMove(delta: number, keys: string[]) { - if (keys.indexOf("w") != -1 || keys.indexOf("ArrowUp".toLowerCase()) != -1) { - socket.send(JSON.stringify(new EventInput(InputEnum.up))); + 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 + } } - if (keys.indexOf("s") != -1 || keys.indexOf("ArrowDown".toLowerCase()) != -1) { - socket.send(JSON.stringify(new EventInput(InputEnum.down))); + 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 } +} - // prediction +function playerMovePrediction(delta: number, input: en.InputEnum) +{ + // client prediction const racket = clientInfo.racket; racket.dir.y = 0; - if (keys.indexOf("w") != -1 || keys.indexOf("ArrowUp".toLowerCase()) != -1) { + if (input === en.InputEnum.up) { racket.dir.y += -1; } - if (keys.indexOf("s") != -1 || keys.indexOf("ArrowDown".toLowerCase()) != -1) { + else if (input === en.InputEnum.down) { racket.dir.y += 1; } racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]); } -export {handleInput} +function repeatInput(lastInputId: number) +{ + // server reconciliation + let i = inputHistoryArr.findIndex((value: InputHistory) => { + if (value.inputId === lastInputId) { + return true; + } + return false; + }); + + // console.log("repeatInput, lastInputId = " + lastInputId); + // console.log("repeatInput, before splice up to index " + i); + // console.log(inputHistoryArr); + + inputHistoryArr.splice(0, i+1); + + // console.log("repeatInput, after splice"); + // console.log(inputHistoryArr); + + inputHistoryArr.forEach((value: InputHistory) => { + playerMovePrediction(value.deltaTime, value.input); + }); +} + +export {handleInput, repeatInput} diff --git a/src/client/pong.ts b/src/client/pong.ts index b1db0f1b..b3e76040 100644 --- a/src/client/pong.ts +++ b/src/client/pong.ts @@ -5,6 +5,7 @@ import {gameLoop} from "./gameLoop.js" import * as c from "./constants.js" import { GameComponentsClient } from "./class/GameComponentsClient.js"; import {countdown} from "./utils.js"; +import {handleInput} from "./handleInput.js"; import {socket} from "./ws.js"; socket; // no-op @@ -38,7 +39,8 @@ function resumeGame() window.addEventListener('keyup', function (e) { pong.deleteKey(e.key); }); - pong.interval = window.setInterval(gameLoop, c.gameLoopIntervalMS); + pong.handleInputInterval = window.setInterval(handleInput, c.handleInputIntervalMS); + pong.gameLoopInterval = window.setInterval(gameLoop, c.gameLoopIntervalMS); } diff --git a/src/client/ws.ts b/src/client/ws.ts index 8add7081..4fe1019a 100644 --- a/src/client/ws.ts +++ b/src/client/ws.ts @@ -3,10 +3,11 @@ import {pong, gc} from "./global.js" import * as ev from "../shared_js/class/Event.js" import {matchmaking, matchmakingComplete, startGame} from "./pong.js"; import * as en from "../shared_js/enums.js" -import { Racket } from "../shared_js/class/Rectangle.js"; +import { RacketClient } from "./class/RectangleClient.js"; import { sleep } from "./utils.js"; import * as c from "./constants.js" import {soundRoblox} from "./audio.js" +import { repeatInput } from "./handleInput.js"; const wsPort = 8042; const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong"; @@ -15,7 +16,7 @@ const socket = new WebSocket(wsUrl, "json"); class ClientInfo { id = ""; side: en.PlayerSide; - racket: Racket; + racket: RacketClient; } export const clientInfo = new ClientInfo(); @@ -24,7 +25,7 @@ socket.addEventListener("open", (event) => { socket.send(JSON.stringify( new ev.ClientAnnounce(en.ClientRole.player, clientInfo.id) )); }); -socket.addEventListener("message", logListener); +// socket.addEventListener("message", logListener); // for testing purpose socket.addEventListener("message", preMatchListener); function logListener(this: WebSocket, event: MessageEvent) { @@ -48,6 +49,7 @@ function preMatchListener(this: WebSocket, event: MessageEvent) { else if (clientInfo.side === en.PlayerSide.right) { clientInfo.racket = gc.playerRight; } + clientInfo.racket.color = "darkgreen"; // for testing purpose socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) )); matchmakingComplete(); break; @@ -65,6 +67,7 @@ function inGameListener(event: MessageEvent) switch (data.type) { case en.EventTypes.gameUpdate: console.log("gameUpdate"); + // setTimeout(gameUpdate, 1000, data as ev.EventGameUpdate); // artificial latency for testing purpose gameUpdate(data as ev.EventGameUpdate); break; case en.EventTypes.scoreUpdate: @@ -78,14 +81,15 @@ function inGameListener(event: MessageEvent) } } -async function gameUpdate(data: ev.EventGameUpdate) +function gameUpdate(data: ev.EventGameUpdate) { - // await sleep(1000); // artificial latency for testing purpose gc.playerLeft.pos.y = Math.floor(data.playerLeft.y); gc.playerRight.pos.y = Math.floor(data.playerRight.y); gc.ball.pos.x = Math.floor(data.ball.x); gc.ball.pos.y = Math.floor(data.ball.y); gc.ball.speed = Math.floor(data.ball.speed); + + repeatInput(data.lastInputId); // server reconciliation } function scoreUpdate(data: ev.EventScoreUpdate) diff --git a/src/server/class/Client.ts b/src/server/class/Client.ts index e6fcfd1d..c244e495 100644 --- a/src/server/class/Client.ts +++ b/src/server/class/Client.ts @@ -6,12 +6,12 @@ import { GameSession } from "./GameSession.js"; class Client { socket: WebSocket; id: string; // Pas indispensable si "socket" a une copie de "id" - isAlive: boolean; + lastInputId: number = 0; + isAlive: boolean = true; gameSession: GameSession; constructor(socket: WebSocket, id: string) { this.socket = socket; this.id = id; - this.isAlive = true; } } diff --git a/src/server/class/GameSession.ts b/src/server/class/GameSession.ts index 0fba43bc..317412a9 100644 --- a/src/server/class/GameSession.ts +++ b/src/server/class/GameSession.ts @@ -50,8 +50,11 @@ class GameSession { clearInterval(s.gameLoopInterval); clearInterval(s.clientsUpdateInterval); } - handleInput(client: ClientPlayer, input: en.InputEnum) { + handleInput(client: ClientPlayer, inputEvent: ev.EventInput) { const gc = this.components; + const input = inputEvent.input; + client.lastInputId = inputEvent.inputId; + client.racket.dir.y = 0; if (input === en.InputEnum.up) { client.racket.dir.y += -1; @@ -96,9 +99,11 @@ class GameSession { type: en.EventTypes.gameUpdate, playerLeft: {y: gc.playerLeft.pos.y}, playerRight: {y: gc.playerRight.pos.y}, - ball: {x: gc.ball.pos.x, y: gc.ball.pos.y, speed: gc.ball.speed} + ball: {x: gc.ball.pos.x, y: gc.ball.pos.y, speed: gc.ball.speed}, + lastInputId: 0 }; s.playersMap.forEach( (client) => { + update.lastInputId = client.lastInputId; client.socket.send(JSON.stringify(update)); }); } diff --git a/src/server/wsServer.ts b/src/server/wsServer.ts index b20989cb..922979ae 100644 --- a/src/server/wsServer.ts +++ b/src/server/wsServer.ts @@ -151,7 +151,7 @@ export function clientInputListener(this: WebSocket, data: string) const input: ev.EventInput = JSON.parse(data); if (input.type === en.EventTypes.clientInput) { const client = clientsMap.get(this.id); - client.gameSession.handleInput(client as ClientPlayer, input.input); + client.gameSession.handleInput(client as ClientPlayer, input); } else { console.log("Invalid clientInput"); diff --git a/src/shared_js/class/Event.ts b/src/shared_js/class/Event.ts index ec214460..da0da306 100644 --- a/src/shared_js/class/Event.ts +++ b/src/shared_js/class/Event.ts @@ -29,6 +29,7 @@ class EventGameUpdate extends ServerEvent { playerLeft = {y: 0}; playerRight = {y: 0}; ball = {x: 0, y: 0, speed: 0}; + lastInputId = 0; constructor() { // TODO: constructor that take GameComponentsServer maybe ? super(en.EventTypes.gameUpdate); } @@ -73,9 +74,11 @@ class ClientAnnounce extends ClientEvent { class EventInput extends ClientEvent { input: en.InputEnum; - constructor(input: en.InputEnum = 0) { + inputId: number; + constructor(input: en.InputEnum = 0, inputId: number = 0) { super(en.EventTypes.clientInput); this.input = input; + this.inputId = inputId; } }