HTML5 Canvas 多人游戏 Socket.io 精灵闪烁

HTML5 Canvas Multiplayer Socket.io Sprite Flickering

我将 socket.io 与 Express 结合使用来渲染 canvas 多人游戏,我从一开始就想使用精灵和像素艺术,但是当我的 canvas 更新时我的精灵有点闪烁。

这是 socket.io 服务器代码,它首先将连接的玩家添加到 players 字典,然后检查 movement 发出以更新玩家在 [=39= 上的位置] :

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

这是客户端,有问题的代码是 (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);

问题是,每当我在循环中用 ctx.clearRect(0, 0, width, height); 清理 canvas 时,我会出现一些闪烁,但只有在使用图像时,如果我使用 canvas 形状,我得到 none.

为什么会这样?

如何避免闪烁?我真的需要使用精灵,简单的形状是不行的。

在多人游戏中使用 socket.io 是否有更好的方法来 "loop" 我错过了? (我阅读了有关 animationframe 的内容,但我并不真正理解如何使用 socket.io 实现它)。

感谢任何一点帮助我一直在努力解决这个问题。

你的代码实际上有比这个小闪烁更大的问题。
您每 60 秒通过网络发送一次图像数据。
使用这种逻辑,您将无法在每一端都达到 60FPS 动画。

首先要做的是创建一个资产管理器,您将在其中加载游戏所需的 spritesheets

在这种情况下,它似乎是一些皮肤。所以 加载一次 HTMLImage 和一个 sprite-sheet 将包含你所有的皮肤 sprite 甚至在游戏开始之前 并制作这个 HTMLImage可用于您的渲染循环,例如将其设置为游戏函数的全局变量。

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

然后你将通过套接字发送的只是这个精灵内部的坐标-sheet其他玩家将不得不绘制。

因为你的 HTMLImage 包含这个 sprite-sheet 已经加载了,你在 socket.on('state' 处理程序中所要做的就是绘制 sprite sheet 的相应部分给定坐标。

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
    );
  }
});