From dc4e0ec0b3641799d6e4fefbf55e267b7c54ecde Mon Sep 17 00:00:00 2001 From: LifeAsDev Date: Sun, 3 Aug 2025 09:38:14 -0400 Subject: [PATCH 1/3] Client-side position interpolation --- src/client/js/app.js | 327 ++++++++++++++++++++++++++++--------------- 1 file changed, 211 insertions(+), 116 deletions(-) diff --git a/src/client/js/app.js b/src/client/js/app.js index 96b4266d..1e38ccbf 100644 --- a/src/client/js/app.js +++ b/src/client/js/app.js @@ -1,11 +1,13 @@ -var io = require('socket.io-client'); -var render = require('./render'); -var ChatClient = require('./chat-client'); -var Canvas = require('./canvas'); -var global = require('./global'); +var io = require("socket.io-client"); +var render = require("./render"); +var ChatClient = require("./chat-client"); +var Canvas = require("./canvas"); +var global = require("./global"); -var playerNameInput = document.getElementById('playerNameInput'); +var playerNameInput = document.getElementById("playerNameInput"); var socket; +let lastServerUpdate = Date.now(); +let interpolationFactor = 1; var debug = function (args) { if (console && console.log) { @@ -18,21 +20,22 @@ if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) { } function startGame(type) { - global.playerName = playerNameInput.value.replace(/(<([^>]+)>)/ig, '').substring(0, 25); + global.playerName = playerNameInput.value + .replace(/(<([^>]+)>)/gi, "") + .substring(0, 25); global.playerType = type; global.screen.width = window.innerWidth; global.screen.height = window.innerHeight; - document.getElementById('startMenuWrapper').style.maxHeight = '0px'; - document.getElementById('gameAreaWrapper').style.opacity = 1; + document.getElementById("startMenuWrapper").style.maxHeight = "0px"; + document.getElementById("gameAreaWrapper").style.opacity = 1; if (!socket) { socket = io({ query: "type=" + type }); setupSocket(socket); } - if (!global.animLoopHandle) - animloop(); - socket.emit('respawn'); + if (!global.animLoopHandle) animloop(); + socket.emit("respawn"); window.chat.socket = socket; window.chat.registerFunctions(); window.canvas.socket = socket; @@ -42,49 +45,47 @@ function startGame(type) { // Checks if the nick chosen contains valid alphanumeric characters (and underscores). function validNick() { var regex = /^\w*$/; - debug('Regex Test', regex.exec(playerNameInput.value)); + debug("Regex Test", regex.exec(playerNameInput.value)); return regex.exec(playerNameInput.value) !== null; } window.onload = function () { - - var btn = document.getElementById('startButton'), - btnS = document.getElementById('spectateButton'), - nickErrorText = document.querySelector('#startMenu .input-error'); + var btn = document.getElementById("startButton"), + btnS = document.getElementById("spectateButton"), + nickErrorText = document.querySelector("#startMenu .input-error"); btnS.onclick = function () { - startGame('spectator'); + startGame("spectator"); }; btn.onclick = function () { - // Checks if the nick is valid. if (validNick()) { nickErrorText.style.opacity = 0; - startGame('player'); + startGame("player"); } else { nickErrorText.style.opacity = 1; } }; - var settingsMenu = document.getElementById('settingsButton'); - var settings = document.getElementById('settings'); + var settingsMenu = document.getElementById("settingsButton"); + var settings = document.getElementById("settings"); settingsMenu.onclick = function () { - if (settings.style.maxHeight == '300px') { - settings.style.maxHeight = '0px'; + if (settings.style.maxHeight == "300px") { + settings.style.maxHeight = "0px"; } else { - settings.style.maxHeight = '300px'; + settings.style.maxHeight = "300px"; } }; - playerNameInput.addEventListener('keypress', function (e) { + playerNameInput.addEventListener("keypress", function (e) { var key = e.which || e.keyCode; if (key === global.KEY_ENTER) { if (validNick()) { nickErrorText.style.opacity = 0; - startGame('player'); + startGame("player"); } else { nickErrorText.style.opacity = 1; } @@ -96,10 +97,10 @@ window.onload = function () { var playerConfig = { border: 6, - textColor: '#FFFFFF', - textBorder: '#000000', + textColor: "#FFFFFF", + textBorder: "#000000", textBorderSize: 3, - defaultSize: 30 + defaultSize: 30, }; var player = { @@ -108,7 +109,7 @@ var player = { y: global.screen.height / 2, screenWidth: global.screen.width, screenHeight: global.screen.height, - target: { x: global.screen.width / 2, y: global.screen.height / 2 } + target: { x: global.screen.width / 2, y: global.screen.height / 2 }, }; global.player = player; @@ -116,6 +117,7 @@ var foods = []; var viruses = []; var fireFood = []; var users = []; +let previousUsers = []; var leaderboard = []; var target = { x: player.x, y: player.y }; global.target = target; @@ -123,53 +125,54 @@ global.target = target; window.canvas = new Canvas(); window.chat = new ChatClient(); -var visibleBorderSetting = document.getElementById('visBord'); +var visibleBorderSetting = document.getElementById("visBord"); visibleBorderSetting.onchange = settings.toggleBorder; -var showMassSetting = document.getElementById('showMass'); +var showMassSetting = document.getElementById("showMass"); showMassSetting.onchange = settings.toggleMass; -var continuitySetting = document.getElementById('continuity'); +var continuitySetting = document.getElementById("continuity"); continuitySetting.onchange = settings.toggleContinuity; -var roundFoodSetting = document.getElementById('roundFood'); +var roundFoodSetting = document.getElementById("roundFood"); roundFoodSetting.onchange = settings.toggleRoundFood; var c = window.canvas.cv; -var graph = c.getContext('2d'); +var graph = c.getContext("2d"); $("#feed").click(function () { - socket.emit('1'); + socket.emit("1"); window.canvas.reenviar = false; }); $("#split").click(function () { - socket.emit('2'); + socket.emit("2"); window.canvas.reenviar = false; }); function handleDisconnect() { socket.close(); - if (!global.kicked) { // We have a more specific error message - render.drawErrorMessage('Disconnected!', graph, global.screen); + if (!global.kicked) { + // We have a more specific error message + render.drawErrorMessage("Disconnected!", graph, global.screen); } } // socket stuff. function setupSocket(socket) { // Handle ping. - socket.on('pongcheck', function () { + socket.on("pongcheck", function () { var latency = Date.now() - global.startPingTime; - debug('Latency: ' + latency + 'ms'); - window.chat.addSystemLine('Ping: ' + latency + 'ms'); + debug("Latency: " + latency + "ms"); + window.chat.addSystemLine("Ping: " + latency + "ms"); }); // Handle error. - socket.on('connect_error', handleDisconnect); - socket.on('disconnect', handleDisconnect); + socket.on("connect_error", handleDisconnect); + socket.on("disconnect", handleDisconnect); // Handle connection. - socket.on('welcome', function (playerSettings, gameSizes) { + socket.on("welcome", function (playerSettings, gameSizes) { player = playerSettings; player.name = global.playerName; player.screenWidth = global.screen.width; @@ -177,12 +180,14 @@ function setupSocket(socket) { player.target = window.canvas.target; global.player = player; window.chat.player = player; - socket.emit('gotit', player); + socket.emit("gotit", player); global.gameStart = true; - window.chat.addSystemLine('Connected to the game!'); - window.chat.addSystemLine('Type -help for a list of commands.'); + window.chat.addSystemLine("Connected to the game!"); + window.chat.addSystemLine("Type -help for a list of commands."); if (global.mobile) { - document.getElementById('gameAreaWrapper').removeChild(document.getElementById('chatbox')); + document + .getElementById("gameAreaWrapper") + .removeChild(document.getElementById("chatbox")); } c.focus(); global.game.width = gameSizes.width; @@ -190,74 +195,113 @@ function setupSocket(socket) { resize(); }); - socket.on('playerDied', (data) => { - const player = isUnnamedCell(data.playerEatenName) ? 'An unnamed cell' : data.playerEatenName; + socket.on("playerDied", (data) => { + const player = isUnnamedCell(data.playerEatenName) + ? "An unnamed cell" + : data.playerEatenName; //const killer = isUnnamedCell(data.playerWhoAtePlayerName) ? 'An unnamed cell' : data.playerWhoAtePlayerName; //window.chat.addSystemLine('{GAME} - ' + (player) + ' was eaten by ' + (killer) + ''); - window.chat.addSystemLine('{GAME} - ' + (player) + ' was eaten'); + window.chat.addSystemLine("{GAME} - " + player + " was eaten"); }); - socket.on('playerDisconnect', (data) => { - window.chat.addSystemLine('{GAME} - ' + (isUnnamedCell(data.name) ? 'An unnamed cell' : data.name) + ' disconnected.'); + socket.on("playerDisconnect", (data) => { + window.chat.addSystemLine( + "{GAME} - " + + (isUnnamedCell(data.name) ? "An unnamed cell" : data.name) + + " disconnected." + ); }); - socket.on('playerJoin', (data) => { - window.chat.addSystemLine('{GAME} - ' + (isUnnamedCell(data.name) ? 'An unnamed cell' : data.name) + ' joined.'); + socket.on("playerJoin", (data) => { + window.chat.addSystemLine( + "{GAME} - " + + (isUnnamedCell(data.name) ? "An unnamed cell" : data.name) + + " joined." + ); }); - socket.on('leaderboard', (data) => { + socket.on("leaderboard", (data) => { leaderboard = data.leaderboard; var status = 'Leaderboard'; for (var i = 0; i < leaderboard.length; i++) { - status += '
'; + status += "
"; if (leaderboard[i].id == player.id) { if (leaderboard[i].name.length !== 0) - status += '' + (i + 1) + '. ' + leaderboard[i].name + ""; + status += + '' + + (i + 1) + + ". " + + leaderboard[i].name + + ""; else - status += '' + (i + 1) + ". An unnamed cell"; + status += + '' + + (i + 1) + + ". An unnamed cell"; } else { if (leaderboard[i].name.length !== 0) - status += (i + 1) + '. ' + leaderboard[i].name; - else - status += (i + 1) + '. An unnamed cell'; + status += i + 1 + ". " + leaderboard[i].name; + else status += i + 1 + ". An unnamed cell"; } } //status += '
Players: ' + data.players; - document.getElementById('status').innerHTML = status; + document.getElementById("status").innerHTML = status; }); - socket.on('serverMSG', function (data) { + socket.on("serverMSG", function (data) { window.chat.addSystemLine(data); }); // Chat. - socket.on('serverSendPlayerChat', function (data) { + socket.on("serverSendPlayerChat", function (data) { window.chat.addChatLine(data.sender, data.message, false); }); // Handle movement. - socket.on('serverTellPlayerMove', function (playerData, userData, foodsList, massList, virusList) { - if (global.playerType == 'player') { - player.x = playerData.x; - player.y = playerData.y; - player.hue = playerData.hue; - player.massTotal = playerData.massTotal; - player.cells = playerData.cells; + socket.on( + "serverTellPlayerMove", + function (playerData, userData, foodsList, massList, virusList) { + if (global.playerType == "player") { + player.x = playerData.x; + player.y = playerData.y; + player.hue = playerData.hue; + player.massTotal = playerData.massTotal; + player.cells = playerData.cells; + } + previousUsers = users.map((u) => ({ + id: u.id, + prevCells: u.cells.map((c) => ({ + x: c.x, + y: c.y, + radius: c.radius, + })), + })); + + users = userData.map((user) => { + let existing = previousUsers.find((u) => u.id === user.id); + return { + ...user, + prevCells: existing ? existing.cells : user.cells, + }; + }); + lastServerUpdate = Date.now(); + + /* users = userData; + */ foods = foodsList; + viruses = virusList; + fireFood = massList; } - users = userData; - foods = foodsList; - viruses = virusList; - fireFood = massList; - }); + ); // Death. - socket.on('RIP', function () { + socket.on("RIP", function () { global.gameStart = false; - render.drawErrorMessage('You died!', graph, global.screen); + render.drawErrorMessage("You died!", graph, global.screen); window.setTimeout(() => { - document.getElementById('gameAreaWrapper').style.opacity = 0; - document.getElementById('startMenuWrapper').style.maxHeight = '1000px'; + document.getElementById("gameAreaWrapper").style.opacity = 0; + document.getElementById("startMenuWrapper").style.maxHeight = + "1000px"; if (global.animLoopHandle) { window.cancelAnimationFrame(global.animLoopHandle); global.animLoopHandle = undefined; @@ -265,14 +309,17 @@ function setupSocket(socket) { }, 2500); }); - socket.on('kick', function (reason) { + socket.on("kick", function (reason) { global.gameStart = false; global.kicked = true; - if (reason !== '') { - render.drawErrorMessage('You were kicked for: ' + reason, graph, global.screen); - } - else { - render.drawErrorMessage('You were kicked!', graph, global.screen); + if (reason !== "") { + render.drawErrorMessage( + "You were kicked for: " + reason, + graph, + global.screen + ); + } else { + render.drawErrorMessage("You were kicked!", graph, global.screen); } socket.close(); }); @@ -283,97 +330,145 @@ const isUnnamedCell = (name) => name.length < 1; const getPosition = (entity, player, screen) => { return { x: entity.x - player.x + screen.width / 2, - y: entity.y - player.y + screen.height / 2 - } -} + y: entity.y - player.y + screen.height / 2, + }; +}; window.requestAnimFrame = (function () { - return window.requestAnimationFrame || + return ( + window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); - }; + } + ); })(); window.cancelAnimFrame = (function (handle) { - return window.cancelAnimationFrame || - window.mozCancelAnimationFrame; + return window.cancelAnimationFrame || window.mozCancelAnimationFrame; })(); function animloop() { global.animLoopHandle = window.requestAnimFrame(animloop); gameLoop(); } +function lerp(a, b, t) { + return a + (b - a) * t; +} function gameLoop() { if (global.gameStart) { graph.fillStyle = global.backgroundColor; + let now = Date.now(); + interpolationFactor = Math.min((now - lastServerUpdate) / 50, 1); graph.fillRect(0, 0, global.screen.width, global.screen.height); render.drawGrid(global, player, global.screen, graph); - foods.forEach(food => { + foods.forEach((food) => { let position = getPosition(food, player, global.screen); render.drawFood(position, food, graph); }); - fireFood.forEach(fireFood => { + fireFood.forEach((fireFood) => { let position = getPosition(fireFood, player, global.screen); render.drawFireFood(position, fireFood, playerConfig, graph); }); - viruses.forEach(virus => { + viruses.forEach((virus) => { let position = getPosition(virus, player, global.screen); render.drawVirus(position, virus, graph); }); - - let borders = { // Position of the borders on the screen + let borders = { + // Position of the borders on the screen left: global.screen.width / 2 - player.x, right: global.screen.width / 2 + global.game.width - player.x, top: global.screen.height / 2 - player.y, - bottom: global.screen.height / 2 + global.game.height - player.y - } + bottom: global.screen.height / 2 + global.game.height - player.y, + }; if (global.borderDraw) { render.drawBorder(borders, graph); } var cellsToDraw = []; for (var i = 0; i < users.length; i++) { - let color = 'hsl(' + users[i].hue + ', 100%, 50%)'; - let borderColor = 'hsl(' + users[i].hue + ', 100%, 45%)'; + let color = "hsl(" + users[i].hue + ", 100%, 50%)"; + let borderColor = "hsl(" + users[i].hue + ", 100%, 45%)"; + let user = users[i]; + for (var j = 0; j < users[i].cells.length; j++) { + let currCell = user.cells[j]; + let prevCell = user.prevCells?.[j]; + let interpX = prevCell + ? lerp(prevCell.x, currCell.x, interpolationFactor) + : currCell.x; + let interpY = prevCell + ? lerp(prevCell.y, currCell.y, interpolationFactor) + : currCell.y; + let interpR = prevCell + ? lerp( + prevCell.radius, + currCell.radius, + interpolationFactor + ) + : currCell.radius; + cellsToDraw.push({ color: color, borderColor: borderColor, mass: users[i].cells[j].mass, name: users[i].name, radius: users[i].cells[j].radius, - x: users[i].cells[j].x - player.x + global.screen.width / 2, - y: users[i].cells[j].y - player.y + global.screen.height / 2 + x: interpX - player.x + global.screen.width / 2, + y: interpY - player.y + global.screen.height / 2, }); } } + previousUsers = users.map((u) => ({ + ...u, + cells: u.cells.map((c) => ({ ...c })), + })); + cellsToDraw.sort(function (obj1, obj2) { return obj1.mass - obj2.mass; }); - render.drawCells(cellsToDraw, playerConfig, global.toggleMassState, borders, graph); - - socket.emit('0', window.canvas.target); // playerSendTarget "Heartbeat". + render.drawCells( + cellsToDraw, + playerConfig, + global.toggleMassState, + borders, + graph + ); + + socket.emit("0", window.canvas.target); // playerSendTarget "Heartbeat". } } -window.addEventListener('resize', resize); +window.addEventListener("resize", resize); function resize() { if (!socket) return; - player.screenWidth = c.width = global.screen.width = global.playerType == 'player' ? window.innerWidth : global.game.width; - player.screenHeight = c.height = global.screen.height = global.playerType == 'player' ? window.innerHeight : global.game.height; - - if (global.playerType == 'spectator') { + player.screenWidth = + c.width = + global.screen.width = + global.playerType == "player" + ? window.innerWidth + : global.game.width; + player.screenHeight = + c.height = + global.screen.height = + global.playerType == "player" + ? window.innerHeight + : global.game.height; + + if (global.playerType == "spectator") { player.x = global.game.width / 2; player.y = global.game.height / 2; } - socket.emit('windowResized', { screenWidth: global.screen.width, screenHeight: global.screen.height }); + socket.emit("windowResized", { + screenWidth: global.screen.width, + screenHeight: global.screen.height, + }); } From 4346fe0cd1c9bb6ca1400aab9c0c46716b4062e9 Mon Sep 17 00:00:00 2001 From: LifeAsDev Date: Sun, 3 Aug 2025 11:14:16 -0400 Subject: [PATCH 2/3] interpolation feature improve --- gulpfile.js | 119 +++++++++++++++++++++++++------------- package.json | 135 ++++++++++++++++++++++--------------------- src/client/js/app.js | 112 +++++++++++++++++++++-------------- 3 files changed, 216 insertions(+), 150 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index ed603121..e7fd6117 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,87 +1,126 @@ /* jshint esversion: 8 */ -const gulp = require('gulp'); -const babel = require('gulp-babel'); -const eslint = require('gulp-eslint'); -const nodemon = require('gulp-nodemon'); -const todo = require('gulp-todo'); -const webpack = require('webpack-stream'); -const Mocha = require('mocha'); +const gulp = require("gulp"); +const babel = require("gulp-babel"); +const eslint = require("gulp-eslint"); +const nodemon = require("gulp-nodemon"); +const todo = require("gulp-todo"); +const webpack = require("webpack-stream"); +const Mocha = require("mocha"); const fs = require("fs"); const path = require("path"); -const PluginError = require('plugin-error'); +const PluginError = require("plugin-error"); function getWebpackConfig() { - return require('./webpack.config.js')(!process.env.IS_DEV) + return require("./webpack.config.js")(!process.env.IS_DEV); } function runServer(done) { nodemon({ delay: 10, - script: './bin/server/server.js', - ignore: ['bin/'], - ext: 'js html css', + script: "./bin/server/server.js", + ignore: ["bin/"], + ext: "js html css", done, - tasks: [process.env.IS_DEV ? 'dev' : 'build'] - }) + tasks: [process.env.IS_DEV ? "dev" : "build"], + }); } function buildServer() { - let task = gulp.src(['src/server/**/*.*', 'src/server/**/*.js']); + let task = gulp.src(["src/server/**/*.*", "src/server/**/*.js"]); if (!process.env.IS_DEV) { - task = task.pipe(babel()) + task = task.pipe(babel()); } - return task.pipe(gulp.dest('bin/server/')); + return task.pipe(gulp.dest("bin/server/")); } function copyClientResources() { - return gulp.src(['src/client/**/*.*', '!src/client/**/*.js']) - .pipe(gulp.dest('./bin/client/')); + return gulp + .src(["src/client/**/*.*", "!src/client/**/*.js"]) + .pipe(gulp.dest("./bin/client/")); } function buildClientJS() { - return gulp.src(['src/client/js/app.js']) + return gulp + .src(["src/client/js/app.js"]) .pipe(webpack(getWebpackConfig())) - .pipe(gulp.dest('bin/client/js/')); + .pipe(gulp.dest("bin/client/js/")); } function setDev(done) { - process.env.IS_DEV = 'true'; + process.env.IS_DEV = "true"; done(); } function mocha(done) { - const mochaInstance = new Mocha() + const mochaInstance = new Mocha(); const files = fs - .readdirSync('test/', {recursive: true}) - .filter(x => x.endsWith('.js')).map(x => path.resolve('test/' + x)); + .readdirSync("test/", { recursive: true }) + .filter((x) => x.endsWith(".js")) + .map((x) => path.resolve("test/" + x)); for (const file of files) { mochaInstance.addFile(file); } - mochaInstance.run(failures => failures ? done(new PluginError('mocha', `${failures} test(s) failed`)) : done()); + mochaInstance.run((failures) => + failures + ? done(new PluginError("mocha", `${failures} test(s) failed`)) + : done() + ); } -gulp.task('lint', () => { - return gulp.src(['**/*.js', '!node_modules/**/*.js', '!bin/**/*.js']) +gulp.task("lint", () => { + return gulp + .src(["**/*.js", "!node_modules/**/*.js", "!bin/**/*.js"]) .pipe(eslint()) .pipe(eslint.format()) - .pipe(eslint.failAfterError()) + .pipe(eslint.failAfterError()); }); -gulp.task('test', gulp.series('lint', mocha)); +gulp.task("test", gulp.series("lint", mocha)); + +gulp.task( + "todo", + gulp.series("lint", () => { + return gulp.src("src/**/*.js").pipe(todo()).pipe(gulp.dest("./")); + }) +); + +gulp.task( + "build", + gulp.series( + "lint", + gulp.parallel(copyClientResources, buildClientJS, buildServer, mocha) + ) +); -gulp.task('todo', gulp.series('lint', () => { - return gulp.src('src/**/*.js') - .pipe(todo()) - .pipe(gulp.dest('./')); -})); +gulp.task( + "dev", + gulp.parallel(copyClientResources, buildClientJS, buildServer) +); -gulp.task('build', gulp.series('lint', gulp.parallel(copyClientResources, buildClientJS, buildServer, mocha))); +gulp.task("run", gulp.series("build", runServer)); -gulp.task('dev', gulp.parallel(copyClientResources, buildClientJS, buildServer)); +gulp.task("watch", gulp.series(setDev, "dev", runServer)); -gulp.task('run', gulp.series('build', runServer)); +gulp.task("default", gulp.series("run")); +// Client: solo JS +function watchClientJS() { + return gulp.watch(["src/client/js/**/*.js"], gulp.series(buildClientJS)); +} + +// Client: HTML, CSS, imágenes, etc. +function watchClientResources() { + return gulp.watch( + ["src/client/**/*.*", "!src/client/**/*.js"], + gulp.series(copyClientResources) + ); +} -gulp.task('watch', gulp.series(setDev, 'dev', runServer)); +// Watch todo el cliente +gulp.task("client:watch", gulp.parallel(watchClientJS, watchClientResources)); -gulp.task('default', gulp.series('run')); +// Corre todo junto en modo dev +gulp.task( + "dev:all", + gulp.series(setDev, "dev", gulp.parallel(runServer, "client:watch")) +); diff --git a/package.json b/package.json index afea02a9..d9b92c42 100644 --- a/package.json +++ b/package.json @@ -1,70 +1,71 @@ { - "name": "agar.io-clone", - "version": "1.0.0", - "description": "A simple Agar.io clone", - "main": "server/server.js", - "scripts": { - "build": "gulp build", - "start": "gulp run", - "watch": "gulp watch", - "test": "gulp test" - }, - "repository": { - "type": "git", - "url": "https://github.com/owenashurst/agar.io-clone.git" - }, - "author": "Huy Tran", - "maintainers": [ - { - "name": "Owen Ashurst" + "name": "agar.io-clone", + "version": "1.0.0", + "description": "A simple Agar.io clone", + "main": "server/server.js", + "scripts": { + "build": "gulp build", + "start": "gulp run", + "watch": "gulp watch", + "test": "gulp test", + "dev:all": "gulp dev:all" + }, + "repository": { + "type": "git", + "url": "https://github.com/owenashurst/agar.io-clone.git" + }, + "author": "Huy Tran", + "maintainers": [ + { + "name": "Owen Ashurst" + } + ], + "license": "MIT", + "contributors": [ + "abalabahaha (https://github.com/abalabahaha)", + "Ariamiro (https://github.com/Ariamiro)", + "Bjarne Oeverli (https://github.com/bjarneo)", + "Chris Morgan (https://github.com/drpotato)", + "Damian Dlugosz (https://github.com/bigfoot90)", + "Dan Prince (https://github.com/danprince)", + "Igor Antun (https://github.com/IgorAntun)", + "Juha Tauriainen (https://github.com/JuhQ)", + "Keith Groves (https://github.com/buskcoin)", + "Kostas Bariotis (https://github.com/kbariotis)", + "Madara Uchiha (https://github.com/MadaraUchiha)", + "Nguyen Huu Thanh (https://github.com/giongto35)", + "PET Computação UFPR (http://pet.inf.ufpr.br)", + "Saren Currie (https://github.com/SarenCurrie)", + "VILLERS Mickaël (https://github.com/villers)", + "wb9688 (https://github.com/wb9688)" + ], + "dependencies": { + "@babel/core": "^7.21.8", + "@babel/eslint-parser": "^7.21.8", + "@babel/preset-env": "^7.21.5", + "babel-loader": "^9.1.2", + "chai": "^4.3.7", + "eslint": "^8.40.0", + "eslint-plugin-import": "^2.27.5", + "express": "^4.18.2", + "gulp": "^4.0.2", + "gulp-babel": "^8.0.0", + "gulp-eslint": "^6.0.0", + "gulp-nodemon": "^2.5.0", + "gulp-todo": "^7.1.1", + "jshint": "^2.13.6", + "mocha": "^10.2.0", + "nodemon": "^3.0.1", + "sat": "^0.9.0", + "socket.io": "^4.6.1", + "socket.io-client": "^4.6.1", + "sqlite3": "^5.1.6", + "uuid": "^9.0.0", + "webpack": "^5.82.1", + "webpack-stream": "^7.0.0" + }, + "devDependencies": { + "@types/sat": "^0.0.32", + "plugin-error": "^2.0.1" } - ], - "license": "MIT", - "contributors": [ - "abalabahaha (https://github.com/abalabahaha)", - "Ariamiro (https://github.com/Ariamiro)", - "Bjarne Oeverli (https://github.com/bjarneo)", - "Chris Morgan (https://github.com/drpotato)", - "Damian Dlugosz (https://github.com/bigfoot90)", - "Dan Prince (https://github.com/danprince)", - "Igor Antun (https://github.com/IgorAntun)", - "Juha Tauriainen (https://github.com/JuhQ)", - "Keith Groves (https://github.com/buskcoin)", - "Kostas Bariotis (https://github.com/kbariotis)", - "Madara Uchiha (https://github.com/MadaraUchiha)", - "Nguyen Huu Thanh (https://github.com/giongto35)", - "PET Computação UFPR (http://pet.inf.ufpr.br)", - "Saren Currie (https://github.com/SarenCurrie)", - "VILLERS Mickaël (https://github.com/villers)", - "wb9688 (https://github.com/wb9688)" - ], - "dependencies": { - "@babel/core": "^7.21.8", - "@babel/eslint-parser": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "babel-loader": "^9.1.2", - "chai": "^4.3.7", - "eslint": "^8.40.0", - "eslint-plugin-import": "^2.27.5", - "express": "^4.18.2", - "gulp": "^4.0.2", - "gulp-babel": "^8.0.0", - "gulp-eslint": "^6.0.0", - "gulp-nodemon": "^2.5.0", - "gulp-todo": "^7.1.1", - "jshint": "^2.13.6", - "mocha": "^10.2.0", - "nodemon": "^3.0.1", - "sat": "^0.9.0", - "socket.io": "^4.6.1", - "socket.io-client": "^4.6.1", - "sqlite3": "^5.1.6", - "uuid": "^9.0.0", - "webpack": "^5.82.1", - "webpack-stream": "^7.0.0" - }, - "devDependencies": { - "@types/sat": "^0.0.32", - "plugin-error": "^2.0.1" - } } diff --git a/src/client/js/app.js b/src/client/js/app.js index 1e38ccbf..cc1b6aed 100644 --- a/src/client/js/app.js +++ b/src/client/js/app.js @@ -110,6 +110,8 @@ var player = { screenWidth: global.screen.width, screenHeight: global.screen.height, target: { x: global.screen.width / 2, y: global.screen.height / 2 }, + targetX: global.screen.width / 2, + targetY: global.screen.height / 2, }; global.player = player; @@ -263,28 +265,37 @@ function setupSocket(socket) { "serverTellPlayerMove", function (playerData, userData, foodsList, massList, virusList) { if (global.playerType == "player") { - player.x = playerData.x; - player.y = playerData.y; + player.targetX = playerData.x; + player.targetY = playerData.y; player.hue = playerData.hue; player.massTotal = playerData.massTotal; player.cells = playerData.cells; + if (typeof player.x !== "number" || isNaN(player.x)) + player.x = player.targetX; + if (typeof player.y !== "number" || isNaN(player.y)) + player.y = player.targetY; } - previousUsers = users.map((u) => ({ - id: u.id, - prevCells: u.cells.map((c) => ({ - x: c.x, - y: c.y, - radius: c.radius, - })), - })); - - users = userData.map((user) => { - let existing = previousUsers.find((u) => u.id === user.id); + users = userData.map((newUser) => { + const existingUser = users.find((u) => u.id === newUser.id); + return { - ...user, - prevCells: existing ? existing.cells : user.cells, + ...newUser, + prevCells: existingUser + ? existingUser.cells + : newUser.cells, + cells: newUser.cells.map((newCell, i) => { + const existingCell = existingUser?.cells[i]; + return { + ...newCell, + renderX: existingCell?.renderX ?? newCell.x, + renderY: existingCell?.renderY ?? newCell.y, + renderRadius: + existingCell?.renderRadius ?? newCell.radius, + }; + }), }; }); + lastServerUpdate = Date.now(); /* users = userData; @@ -357,12 +368,30 @@ function animloop() { function lerp(a, b, t) { return a + (b - a) * t; } +let lastFrameTime = Date.now(); +let fpsCounter = 0; +let lastFpsUpdate = Date.now(); +let currentFps = 0; function gameLoop() { if (global.gameStart) { graph.fillStyle = global.backgroundColor; let now = Date.now(); - interpolationFactor = Math.min((now - lastServerUpdate) / 50, 1); + let deltaTime = (now - lastFrameTime) / 1000; + lastFrameTime = now; + // Lógica para contar los FPS + fpsCounter++; + if (now - lastFpsUpdate >= 1000) { + currentFps = fpsCounter; + fpsCounter = 0; + lastFpsUpdate = now; + } + // Factor de interpolación o suavizado, ej 0.25 + const smoothFactor = 0.25; + console.log("playerX: " + player.x, player.targetX); + player.x = lerp(player.x, player.targetX, smoothFactor); + player.y = lerp(player.y, player.targetY, smoothFactor); + graph.fillRect(0, 0, global.screen.width, global.screen.height); render.drawGrid(global, player, global.screen, graph); @@ -396,38 +425,35 @@ function gameLoop() { let borderColor = "hsl(" + users[i].hue + ", 100%, 45%)"; let user = users[i]; - for (var j = 0; j < users[i].cells.length; j++) { - let currCell = user.cells[j]; - let prevCell = user.prevCells?.[j]; - let interpX = prevCell - ? lerp(prevCell.x, currCell.x, interpolationFactor) - : currCell.x; - let interpY = prevCell - ? lerp(prevCell.y, currCell.y, interpolationFactor) - : currCell.y; - let interpR = prevCell - ? lerp( - prevCell.radius, - currCell.radius, - interpolationFactor - ) - : currCell.radius; + for (var j = 0; j < user.cells.length; j++) { + let cell = user.cells[j]; + cell.renderX = lerp( + cell.renderX ?? cell.x, + cell.x, + smoothFactor + ); + cell.renderY = lerp( + cell.renderY ?? cell.y, + cell.y, + smoothFactor + ); + cell.renderRadius = lerp( + cell.renderRadius ?? cell.radius, + cell.radius, + 0.25 + ); cellsToDraw.push({ - color: color, - borderColor: borderColor, - mass: users[i].cells[j].mass, - name: users[i].name, - radius: users[i].cells[j].radius, - x: interpX - player.x + global.screen.width / 2, - y: interpY - player.y + global.screen.height / 2, + color, + borderColor, + mass: cell.mass, + name: user.name, + radius: cell.renderRadius, + x: cell.renderX - player.x + global.screen.width / 2, + y: cell.renderY - player.y + global.screen.height / 2, }); } } - previousUsers = users.map((u) => ({ - ...u, - cells: u.cells.map((c) => ({ ...c })), - })); cellsToDraw.sort(function (obj1, obj2) { return obj1.mass - obj2.mass; From 96298411c137fd62a71712c3aca5d176bfd38480 Mon Sep 17 00:00:00 2001 From: LifeAsDev Date: Mon, 4 Aug 2025 12:15:01 -0400 Subject: [PATCH 3/3] check internet class --- src/client/js/app.js | 5 +- src/client/js/connectionMonitor.js | 73 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/client/js/connectionMonitor.js diff --git a/src/client/js/app.js b/src/client/js/app.js index cc1b6aed..2ec1d1eb 100644 --- a/src/client/js/app.js +++ b/src/client/js/app.js @@ -3,6 +3,7 @@ var render = require("./render"); var ChatClient = require("./chat-client"); var Canvas = require("./canvas"); var global = require("./global"); +var ConnectionMonitor = require("./connectionMonitor"); var playerNameInput = document.getElementById("playerNameInput"); var socket; @@ -18,6 +19,7 @@ var debug = function (args) { if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) { global.mobile = true; } +let monitor; function startGame(type) { global.playerName = playerNameInput.value @@ -32,6 +34,8 @@ function startGame(type) { document.getElementById("gameAreaWrapper").style.opacity = 1; if (!socket) { socket = io({ query: "type=" + type }); + monitor = new ConnectionMonitor(socket); + setupSocket(socket); } if (!global.animLoopHandle) animloop(); @@ -388,7 +392,6 @@ function gameLoop() { } // Factor de interpolación o suavizado, ej 0.25 const smoothFactor = 0.25; - console.log("playerX: " + player.x, player.targetX); player.x = lerp(player.x, player.targetX, smoothFactor); player.y = lerp(player.y, player.targetY, smoothFactor); diff --git a/src/client/js/connectionMonitor.js b/src/client/js/connectionMonitor.js new file mode 100644 index 00000000..245fdce8 --- /dev/null +++ b/src/client/js/connectionMonitor.js @@ -0,0 +1,73 @@ +class ConnectionMonitor { + constructor(socket) { + this.socket = socket; + this.pings = []; + this.lastPing = null; + this.lastUpdate = Date.now(); + this.updatesReceived = 0; + this.seq = 0; + + // Ping-Pong check + this.socket.on("pongcheck", () => { + const ping = Date.now() - this.lastPing; + + this.pings.push(ping); + if (this.pings.length > 20) this.pings.shift(); + }); + + // Server update tracking + this.socket.on("serverTellPlayerMove", () => { + this.lastUpdate = Date.now(); + this.updatesReceived++; + }); + + // Start monitoring + setInterval(() => this.sendPing(), 1000); + setInterval(() => this.report(), 1000); + } + + sendPing() { + this.lastPing = Date.now(); + this.socket.emit("pingcheck"); + } + + getPingStats() { + if (this.pings.length === 0) return { avg: 0, jitter: 0 }; + const avg = this.pings.reduce((a, b) => a + b, 0) / this.pings.length; + const jitter = Math.max(...this.pings) - Math.min(...this.pings); + return { avg, jitter }; + } + + getUpdateAge() { + return Date.now() - this.lastUpdate; + } + + report() { + const { avg, jitter } = this.getPingStats(); + const updateAge = this.getUpdateAge(); + + const quality = this.getConnectionQuality( + avg, + jitter, + updateAge, + this.updatesReceived + ); + + console.log( + `[Connection] Ping: ${avg.toFixed(1)}ms | Jitter: ${jitter.toFixed( + 1 + )}ms | Updates/s: ${ + this.updatesReceived + } | Last update: ${updateAge}ms ago | Quality: ${quality}` + ); + + this.updatesReceived = 0; + } + + getConnectionQuality(ping, jitter, updateAge, updatesPerSec) { + if (updateAge > 500 || updatesPerSec < 5) return "🟥 Bad"; + if (ping > 200 || jitter > 75) return "🟧 Medium"; + return "🟩 Good"; + } +} +module.exports = ConnectionMonitor;