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

@@ -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;
}
}

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.
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

View File

@@ -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}`);

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 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);
}
});
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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");

View File

@@ -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;
}
}

View File

@@ -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 {

View File

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