0
votes

I'm using socket.io in combination with Express to render a canvas multiplayer game, I wanted to use sprites and pixel art since the beginning, but when my canvas updates my sprite flickers a bit.

Here's the socket.io server code which first adds the connected player to players dictionary and then checks for movement emit in order to update players' position on the canvas :

    let players = {};

        io.on('connection', function (socket) {

          players[socket.id] = { x: 300, y: 300};

          socket.on('disconnect', function(){
           delete players[socket.id];

          });

         socket.on('movement', function(data) {
          let player = players[socket.id] || {};
          if (data.left) {
            player.x -= 5;
          }
          if (data.up) {
            player.y -= 5;
          }
          if (data.right) {
            player.x += 5;
          }
          if (data.down) {
            player.y += 5;
          }
        });

      });

    // Here the server emits\send the "state" of the canvas 
    // looping passing the dictionary of players

    setInterval(function() {
      io.emit('state', players);
    }, 1000 / 60);

And here's the client, where the problematic code is (imo):

var socket = io();

var width = window.innerWidth;
var height = window.innerHeight;

var movement = {
  up: false,
  down: false,
  left: false,
  right: false
}
document.addEventListener('keydown', function(event) {
  switch (event.keyCode) {
    case 65: // A
      movement.left = true;
      console.log("A");
      break;
    case 87: // W
      movement.up = true;
      console.log("W");
      break;
    case 68: // D
      movement.right = true;
      console.log("D");
      break;
    case 83: // S
      movement.down = true;
      console.log("S");
      break;
  }
});
document.addEventListener('keyup', function(event) {
  switch (event.keyCode) {
    case 65: // A
      movement.left = false;
      break;
    case 87: // W
      movement.up = false;
      break;
    case 68: // D
      movement.right = false;
      break;
    case 83: // S
      movement.down = false;
      break;
  }
});
/* end movements */
let canvas = document.getElementById('canvas');
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext('2d');

socket.on('state', function(players) {

    // Clear the canvas so that the previous positions are erased
    ctx.clearRect(0, 0, width, height);

    // Everytime you update (frame) the state of the canvas draw all players on it
    for (let id in players) {
    let player = players[id];
    // Drawing with fillRect = no flickering
    //ctx.fillRect(player.x, player.y, 100, 100);

    // Drawing my sprite image = flickering!

    let image = new Image();

    image.onload = function() {
     ctx.mozImageSmoothingEnabled = true;
     ctx.webkitImageSmoothingEnabled = true;
     ctx.msImageSmoothingEnabled = true;
     ctx.imageSmoothingEnabled = true;
     ctx.drawImage(image, player.x, player.y);
                            };
    image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAqCAYAAABYzsDTAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAUxJREFUWEftlk0KwjAQhb1CL9EjiD+4cyciint37goiiFAE1y506xkEV56gB/AC4jXcuIq8YmRME5MJqSJUeBT/vnnzOmlSq/3yNes3BDQfNHMthq1cy1H7TV4eK/hbbEmvLiDXWNJxR0BO2ZcKVxzAkU1OptUfvaD340qI07ogfO5Fpo6/Bu92NwJCJ7h6OZcxoAPqPAgcwPN2muceHH47pEIqOPy6T7RwOjXe01LBC6Max7GA5EKS73FF5uQ73phTkA5OC0dRxFtEX4F/ci1juewmPOd02aMA7YRGAjALLsGmmxkETh+3NufO7l0jkfMHcJZluawzyYUDCLA1f07eqktnuEve1ggKu/FzA3bY7e35WqrL54rpRMA2T/9QKhyF1AKmguwu1FOWrhgbSiG6Y5wXUJd3qfBgUZjaDXYTdQX+H15KB7p5t47jAzRfydFOtcn9AAAAAElFTkSuQmCC';
        }

});

setInterval(function() {
  socket.emit('movement', movement);
}, 1000 / 60);

The thing is that whenever I clean the canvas with ctx.clearRect(0, 0, width, height);in my loop, I get some flickering, but only when using an image, if I use canvas shapes I get none.

Why is that so?

How do I avoid that flickering? I really need to use sprites and simple shapes are a no-no.

Is there a better way to "loop" in multiplayer using socket.io that I'm missing? (I read about animationframe but I didn't really understand how to implement it with socket.io).

Any little help is appreciated I've been struggling a lot with it.

1
Have you looked into double buffering? I had a canvas game with a similar problem where that solved it. It was local only and not as complex as yours so that may not be your issue.IrkenInvader
I think that as long as canvas shapes don't return flickering or such, double buffering might be a bit overkill...K3nzie
Can't try, but instead of clearing your canvas when setting your image's src, do it in its onload event handler. That should get rid of the flickering. But I am not too clear as if you are drawing data passed from socket or if you are really always drawing this hard-coded b64 image. If the second case, then you are doing it wrong: load your image once, store it in some accessible place, and draw it directly without changing its src ever during your game.Kaiido
@Kaiido thanks for the suggestion, yea it should load data from server since images will be different for each players, in this case im in local it's just the same image, so I just changed src after onload (as I saw in many many code examples, that's why). Gonna do a few tests and report back.K3nzie
Oh great, on the server side javascript can't understand new Image() , it throws an undefined, so I can't set src of an image object befpre everything else this way... how do I do?... Ouch. Anyway, clearing canvas on load of the image solved the major flickering issues, so thanks for that :)K3nzie

1 Answers

0
votes

You actually have a larger problem in your code than this small flickering.
You are sending image data over the network every 60th of a second.
You won't be able to reach 60FPS animation on every end with such logic.

The first thing to do, is to create an assets manager, where you will load the required spritesheets for the game.

In this case, it seems to be some skins. So load once an HTMLImage with a sprite-sheet that will contain all your skins sprites before the game even starts and make this HTMLImage available for your rendering loop, e.g by setting it as a global of your game's function.

const assets = {
  skin: new Image();
};
assets.skin.onload = e => tellTheGameWeAreReady();
assets.skin.src = 'path/to/skin_spritesheet.png';

Then what you will send over the socket is only the coordinates inside this sprite-sheet that the other players will have to draw.

Since your HTMLImage containing this sprite-sheet will already have loaded, all you have to do in the socket.on('state' handler is to draw the corresponding part of the sprite sheet at given coordinates.

socket.on('state', function(players) {
  ctx.clearRect(0, 0, width, height);
  for( let id in players ) {
    let player = players[id];
    /*//socket should emit each 'player' as 
    {
      x: x_position of player in world,
      y: y_position of player in world,
      skin: {
        x: x_position of to be displayed sprite in the sprite-sheet
        y: y_position of to be displayed sprite in the sprite-sheet
        w: width of to be displayed sprite in the sprite-sheet
        h: height of to be displayed sprite in the sprite-sheet
      }
    }
    */
    ctx.drawImage( assets.skins, 
      player.skin.x,
      player.skin.y,
      player.skin.w,
      player.skin.h,
      player.x,
      player.y,
      player.skin.w,
      player.skin.h
    );
  }
});