I'm learning how to use websocket and the socket.io library. I am creating a test 2d online game. In the game, the user controls a ball. The communication scheme is as follows:
"new player"
to the server, if a new player is connected, transmits socket.id
."movement"
to the server, if the player presses/unpresses the control keys, sends the object of the pressed keys of the form { up: boolean, down: boolean, right: boolean, left: boolean, ID: socket.id }
."state"
should render all players connected to the game, but for some reason it does not happen (I see only myself in the game)."new player"
, stores in the players
object the coordinates of the connected player: [socket.id from the client]: {x: number, y: number}
."movement"
, changes the coordinates of the player in the players
object according to the keys pressed."state"
message to the client every 16,67
milliseconds.game.js
code:
var socket = io();
socket.on("connect", function () {
socket.emit("new player", socket.id);
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const gridSize = 50;
const playerSize = 20;
const speed = 25;
let playerX;
let playerY;
let movement = {
up: false,
down: false,
left: false,
right: false,
shift: false,
ID: socket.id
};
function drawGrid() {
let colorIndex = 0;
for (let x = 0; x < canvas.width; x += gridSize) {
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.fillStyle = "green";
ctx.fillRect(x, y, gridSize, gridSize);
}
}
}
function drawPlayer() {
if (!playerX) return;
if (!playerY) return;
ctx.beginPath();
ctx.arc(playerX, playerY, playerSize, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
}
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function updateCamera() {
if (!playerX) return;
if (!playerY) return;
const cameraX = canvas.width / 2 - playerX;
const cameraY = canvas.height / 2 - playerY;
ctx.translate(cameraX, cameraY);
}
function update() {
clearCanvas();
updateCamera();
drawGrid();
drawPlayer();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
function handleKeyPress(event) {
switch (event.key) {
case "a":
movement.left = true;
break;
case "d":
movement.right = true;
break;
case "w":
movement.up = true;
break;
case "s":
movement.down = true;
break;
case "Shift":
movement.shift = true;
break;
}
}
function handleKeyRelease(event) {
switch (event.key) {
case "a":
movement.left = false;
break;
case "d":
movement.right = false;
break;
case "w":
movement.up = false;
break;
case "s":
movement.down = false;
break;
case "Shift":
movement.shift = false;
break;
}
}
window.addEventListener("keydown", handleKeyPress);
window.addEventListener("keyup", handleKeyRelease);
function gameLoop() {
update();
requestAnimationFrame(gameLoop);
socket.emit("movement", movement);
}
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
update();
});
gameLoop();
socket.on("state", function (players) {
clearCanvas();
playerX = players[socket.id]?.x;
playerY = players[socket.id]?.y;
ctx.fillStyle = "yellow";
for (var id in players) {
if (id === socket.id) continue;
var player = players[id];
ctx.beginPath();
ctx.arc(player.x, player.y, playerSize, 0, 2 * Math.PI);
ctx.fill();
}
});
});
server.js
code:
let express = require('express');
let http = require('http');
let path = require('path');
let socketIO = require('socket.io');
let app = express();
let server = http.Server(app);
let io = socketIO(server);
let port = 5000;
app.set('port', port);
app.use('/static', express.static(__dirname + '/static'));
app.get('/', function (request, response) {
response.sendFile(path.join(__dirname, 'index.html'));
});
server.listen(port, function () {
console.log('Starting server on port: ' + port);
});
var players = {};
io.on('connection', function (socket) {
socket.on('new player', function (ID) {
players[ID] = {
x: 959,
y: 496.5
};
});
socket.on('movement', function (data) {
var player = players[data.ID] || {};
if (data.left) {
if (data.shift) {
player.x -= 7.5
} else {
player.x -= 15;
}
}
if (data.up) {
player.y -= 15;
}
if (data.right) {
if (data.shift) {
player.x += 7.5
} else {
player.x += 15;
}
}
if (data.down) {
player.y += 15;
}
});
});
setInterval(function () {
io.sockets.emit('state', players);
}, 1000 / 60);
index.html
code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canvas Game</title>
<style>
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
background-color: #f0f0f0;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script src="/socket.io/socket.io.js"></script>
<script src="static/game.js"></script>
</body>
</html>
I can't understand what exactly the problem is. I think I have configured websocket communication correctly. But still, when a few people enter the game, they do not see each other in the game for some reason.
Game preview (the red ball is a player):
The rule of thumb in most animations is: keep all position updates and rendering in the update loop so you can control ordering. Any rendering code outside the loop fights the main loop for priority. Callbacks should only update state that the rendering loop can read and take into consideration for updating positions and rendering the next frame.
Your main requestAnimationFrame
loop wipes the canvas and only draws one player:
function update() {
clearCanvas();
updateCamera();
drawGrid();
drawPlayer();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
This runs ~60 times a second, destroying the drawing you're doing in socket.on("state", function (players) {
instantly.
Here's the most direct fix:
let players = {};
function update() {
clearCanvas();
updateCamera();
drawGrid();
ctx.fillStyle = "yellow";
for (const id in players) {
if (id === socket.id) continue;
const player = players[id];
ctx.beginPath();
ctx.arc(player.x, player.y, playerSize, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
drawPlayer();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
// ...
socket.on("state", function (_players) {
playerX = _players[socket.id]?.x;
playerY = _players[socket.id]?.y;
players = _players;
});
There are other issues (like never removing players when they disconnect), and room for improvement in the general design (there's no need to emit movement if none happened, for example, and no need to redraw if no update has arrived from the server), but to unblock you on the immediate issue, I'll leave it at this.