server reconciliation OK (a little rubberbanding)
This commit is contained in:
@@ -2,12 +2,12 @@
|
|||||||
import * as c from ".././constants.js"
|
import * as c from ".././constants.js"
|
||||||
|
|
||||||
class GameArea {
|
class GameArea {
|
||||||
keys: string[];
|
keys: string[] = [];
|
||||||
interval: number = 0;
|
handleInputInterval: number = 0;
|
||||||
|
gameLoopInterval: number = 0;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
ctx: CanvasRenderingContext2D;
|
ctx: CanvasRenderingContext2D;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.keys = [];
|
|
||||||
this.canvas = document.createElement("canvas");
|
this.canvas = document.createElement("canvas");
|
||||||
this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
|
this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
this.canvas.width = c.CanvasWidth;
|
this.canvas.width = c.CanvasWidth;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const scoreSize = Math.floor(w/16);
|
|||||||
export const gridSize = Math.floor(w/500);
|
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 gameLoopIntervalMS = 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
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {gc} from "./global.js";
|
import {gc} from "./global.js";
|
||||||
import * as d from "./draw.js";
|
import * as d from "./draw.js";
|
||||||
import {handleInput} from "./handleInput.js";
|
|
||||||
|
|
||||||
let actual_time: number = Date.now();
|
let actual_time: number = Date.now();
|
||||||
let last_time: number;
|
let last_time: number;
|
||||||
@@ -17,9 +16,7 @@ function gameLoop()
|
|||||||
actual_time = Date.now();
|
actual_time = Date.now();
|
||||||
delta_time = (actual_time - last_time) / 1000;
|
delta_time = (actual_time - last_time) / 1000;
|
||||||
|
|
||||||
handleInput(delta_time);
|
// client prediction
|
||||||
|
|
||||||
// prediction
|
|
||||||
gc.ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
|
gc.ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
|
||||||
|
|
||||||
d.draw();
|
d.draw();
|
||||||
|
|||||||
@@ -1,13 +1,42 @@
|
|||||||
import {pong, gc, clientInfo} from "./global.js"
|
import {pong, gc, clientInfo} from "./global.js"
|
||||||
import * as d from "./draw.js";
|
import * as d from "./draw.js";
|
||||||
import { socket } from "./ws.js";
|
import { socket } from "./ws.js";
|
||||||
import {InputEnum} from "../shared_js/enums.js"
|
import * as ev from "../shared_js/class/Event.js"
|
||||||
import {EventInput} from "../shared_js/class/Event.js"
|
import * as en from "../shared_js/enums.js"
|
||||||
|
|
||||||
export let gridDisplay = false;
|
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;
|
var keys = pong.keys;
|
||||||
if (keys.length == 0)
|
if (keys.length == 0)
|
||||||
return;
|
return;
|
||||||
@@ -22,28 +51,63 @@ function handleInput(delta: number)
|
|||||||
gridDisplay = !gridDisplay;
|
gridDisplay = !gridDisplay;
|
||||||
pong.deleteKey("g");
|
pong.deleteKey("g");
|
||||||
}
|
}
|
||||||
playerMove(delta, keys);
|
playerMove(delta_time, keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
function playerMove(delta: number, keys: string[])
|
function playerMove(delta: number, keys: string[])
|
||||||
{
|
{
|
||||||
if (keys.indexOf("w") != -1 || keys.indexOf("ArrowUp".toLowerCase()) != -1) {
|
if (keys.indexOf("w") !== -1 || keys.indexOf("ArrowUp".toLowerCase()) !== -1) {
|
||||||
socket.send(JSON.stringify(new EventInput(InputEnum.up)));
|
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) {
|
else if (keys.indexOf("s") !== -1 || keys.indexOf("ArrowDown".toLowerCase()) !== -1) {
|
||||||
socket.send(JSON.stringify(new EventInput(InputEnum.down)));
|
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;
|
const racket = clientInfo.racket;
|
||||||
racket.dir.y = 0;
|
racket.dir.y = 0;
|
||||||
if (keys.indexOf("w") != -1 || keys.indexOf("ArrowUp".toLowerCase()) != -1) {
|
if (input === en.InputEnum.up) {
|
||||||
racket.dir.y += -1;
|
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.dir.y += 1;
|
||||||
}
|
}
|
||||||
racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
|
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}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {gameLoop} from "./gameLoop.js"
|
|||||||
import * as c from "./constants.js"
|
import * as c from "./constants.js"
|
||||||
import { GameComponentsClient } from "./class/GameComponentsClient.js";
|
import { GameComponentsClient } from "./class/GameComponentsClient.js";
|
||||||
import {countdown} from "./utils.js";
|
import {countdown} from "./utils.js";
|
||||||
|
import {handleInput} from "./handleInput.js";
|
||||||
|
|
||||||
import {socket} from "./ws.js"; socket; // no-op
|
import {socket} from "./ws.js"; socket; // no-op
|
||||||
|
|
||||||
@@ -38,7 +39,8 @@ function resumeGame()
|
|||||||
window.addEventListener('keyup', function (e) {
|
window.addEventListener('keyup', function (e) {
|
||||||
pong.deleteKey(e.key);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import {pong, gc} from "./global.js"
|
|||||||
import * as ev from "../shared_js/class/Event.js"
|
import * as ev from "../shared_js/class/Event.js"
|
||||||
import {matchmaking, matchmakingComplete, startGame} from "./pong.js";
|
import {matchmaking, matchmakingComplete, startGame} from "./pong.js";
|
||||||
import * as en from "../shared_js/enums.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 { sleep } from "./utils.js";
|
||||||
import * as c from "./constants.js"
|
import * as c from "./constants.js"
|
||||||
import {soundRoblox} from "./audio.js"
|
import {soundRoblox} from "./audio.js"
|
||||||
|
import { repeatInput } from "./handleInput.js";
|
||||||
|
|
||||||
const wsPort = 8042;
|
const wsPort = 8042;
|
||||||
const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong";
|
const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong";
|
||||||
@@ -15,7 +16,7 @@ const socket = new WebSocket(wsUrl, "json");
|
|||||||
class ClientInfo {
|
class ClientInfo {
|
||||||
id = "";
|
id = "";
|
||||||
side: en.PlayerSide;
|
side: en.PlayerSide;
|
||||||
racket: Racket;
|
racket: RacketClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clientInfo = new ClientInfo();
|
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.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);
|
socket.addEventListener("message", preMatchListener);
|
||||||
|
|
||||||
function logListener(this: WebSocket, event: MessageEvent) {
|
function logListener(this: WebSocket, event: MessageEvent) {
|
||||||
@@ -48,6 +49,7 @@ function preMatchListener(this: WebSocket, event: MessageEvent) {
|
|||||||
else if (clientInfo.side === en.PlayerSide.right) {
|
else if (clientInfo.side === en.PlayerSide.right) {
|
||||||
clientInfo.racket = gc.playerRight;
|
clientInfo.racket = gc.playerRight;
|
||||||
}
|
}
|
||||||
|
clientInfo.racket.color = "darkgreen"; // for testing purpose
|
||||||
socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) ));
|
socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) ));
|
||||||
matchmakingComplete();
|
matchmakingComplete();
|
||||||
break;
|
break;
|
||||||
@@ -65,6 +67,7 @@ function inGameListener(event: MessageEvent)
|
|||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case en.EventTypes.gameUpdate:
|
case en.EventTypes.gameUpdate:
|
||||||
console.log("gameUpdate");
|
console.log("gameUpdate");
|
||||||
|
// setTimeout(gameUpdate, 1000, data as ev.EventGameUpdate); // artificial latency for testing purpose
|
||||||
gameUpdate(data as ev.EventGameUpdate);
|
gameUpdate(data as ev.EventGameUpdate);
|
||||||
break;
|
break;
|
||||||
case en.EventTypes.scoreUpdate:
|
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.playerLeft.pos.y = Math.floor(data.playerLeft.y);
|
||||||
gc.playerRight.pos.y = Math.floor(data.playerRight.y);
|
gc.playerRight.pos.y = Math.floor(data.playerRight.y);
|
||||||
gc.ball.pos.x = Math.floor(data.ball.x);
|
gc.ball.pos.x = Math.floor(data.ball.x);
|
||||||
gc.ball.pos.y = Math.floor(data.ball.y);
|
gc.ball.pos.y = Math.floor(data.ball.y);
|
||||||
gc.ball.speed = Math.floor(data.ball.speed);
|
gc.ball.speed = Math.floor(data.ball.speed);
|
||||||
|
|
||||||
|
repeatInput(data.lastInputId); // server reconciliation
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreUpdate(data: ev.EventScoreUpdate)
|
function scoreUpdate(data: ev.EventScoreUpdate)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { GameSession } from "./GameSession.js";
|
|||||||
class Client {
|
class Client {
|
||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
id: string; // Pas indispensable si "socket" a une copie de "id"
|
id: string; // Pas indispensable si "socket" a une copie de "id"
|
||||||
isAlive: boolean;
|
lastInputId: number = 0;
|
||||||
|
isAlive: boolean = true;
|
||||||
gameSession: GameSession;
|
gameSession: GameSession;
|
||||||
constructor(socket: WebSocket, id: string) {
|
constructor(socket: WebSocket, id: string) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.isAlive = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,11 @@ class GameSession {
|
|||||||
clearInterval(s.gameLoopInterval);
|
clearInterval(s.gameLoopInterval);
|
||||||
clearInterval(s.clientsUpdateInterval);
|
clearInterval(s.clientsUpdateInterval);
|
||||||
}
|
}
|
||||||
handleInput(client: ClientPlayer, input: en.InputEnum) {
|
handleInput(client: ClientPlayer, inputEvent: ev.EventInput) {
|
||||||
const gc = this.components;
|
const gc = this.components;
|
||||||
|
const input = inputEvent.input;
|
||||||
|
client.lastInputId = inputEvent.inputId;
|
||||||
|
|
||||||
client.racket.dir.y = 0;
|
client.racket.dir.y = 0;
|
||||||
if (input === en.InputEnum.up) {
|
if (input === en.InputEnum.up) {
|
||||||
client.racket.dir.y += -1;
|
client.racket.dir.y += -1;
|
||||||
@@ -96,9 +99,11 @@ class GameSession {
|
|||||||
type: en.EventTypes.gameUpdate,
|
type: en.EventTypes.gameUpdate,
|
||||||
playerLeft: {y: gc.playerLeft.pos.y},
|
playerLeft: {y: gc.playerLeft.pos.y},
|
||||||
playerRight: {y: gc.playerRight.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) => {
|
s.playersMap.forEach( (client) => {
|
||||||
|
update.lastInputId = client.lastInputId;
|
||||||
client.socket.send(JSON.stringify(update));
|
client.socket.send(JSON.stringify(update));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function clientInputListener(this: WebSocket, data: string)
|
|||||||
const input: ev.EventInput = JSON.parse(data);
|
const input: ev.EventInput = JSON.parse(data);
|
||||||
if (input.type === en.EventTypes.clientInput) {
|
if (input.type === en.EventTypes.clientInput) {
|
||||||
const client = clientsMap.get(this.id);
|
const client = clientsMap.get(this.id);
|
||||||
client.gameSession.handleInput(client as ClientPlayer, input.input);
|
client.gameSession.handleInput(client as ClientPlayer, input);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("Invalid clientInput");
|
console.log("Invalid clientInput");
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class EventGameUpdate extends ServerEvent {
|
|||||||
playerLeft = {y: 0};
|
playerLeft = {y: 0};
|
||||||
playerRight = {y: 0};
|
playerRight = {y: 0};
|
||||||
ball = {x: 0, y: 0, speed: 0};
|
ball = {x: 0, y: 0, speed: 0};
|
||||||
|
lastInputId = 0;
|
||||||
constructor() { // TODO: constructor that take GameComponentsServer maybe ?
|
constructor() { // TODO: constructor that take GameComponentsServer maybe ?
|
||||||
super(en.EventTypes.gameUpdate);
|
super(en.EventTypes.gameUpdate);
|
||||||
}
|
}
|
||||||
@@ -73,9 +74,11 @@ class ClientAnnounce extends ClientEvent {
|
|||||||
|
|
||||||
class EventInput extends ClientEvent {
|
class EventInput extends ClientEvent {
|
||||||
input: en.InputEnum;
|
input: en.InputEnum;
|
||||||
constructor(input: en.InputEnum = 0) {
|
inputId: number;
|
||||||
|
constructor(input: en.InputEnum = 0, inputId: number = 0) {
|
||||||
super(en.EventTypes.clientInput);
|
super(en.EventTypes.clientInput);
|
||||||
this.input = input;
|
this.input = input;
|
||||||
|
this.inputId = inputId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user