javascriptnode.jswebsocketsocket.io2d-games

Players connected to the game are not rendered in the browser online game


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:

Client (game.js):

Server (server.js):

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

Image


Solution

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