仅在可见画布区域上绘制tilemap - 优化

时间:2018-02-09 16:51:14

标签: javascript canvas

我想出了如何只绘制画布内的图像(javascript tilemap游戏)。但是不确定这是否足够优化,尽管它会如此。关于如何使其更优化的任何想法?

目前我使用地图数组循环Y和X,然后对于Y中的每个X,我使用带位置坐标的drawImage。我在绘制之前放了一个if语句来检查当前的X和Y是否在画布中。如果是,则绘制图像。这里有一些代码可以显示,并且会在一瞬间提供测试它的链接。

            var mapArray=[

                [3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3],
                [3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
                [3,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,3],
                [3,0,1,1,1,1,0,0,0,0,1,0,1,1,1,1,0,0,0,0,1,3],
                [3,0,1,0,0,1,1,1,0,0,0,0,1,0,0,1,1,1,0,0,0,3],
                [3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
                [3,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,3],
                [3,0,1,1,1,1,0,0,0,0,1,0,1,1,1,1,0,0,0,0,1,3],
                [3,0,1,0,0,1,1,1,0,0,0,0,1,0,0,1,1,1,0,0,0,3],
                [3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
                [3,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,3],
                [3,0,1,1,1,1,0,0,0,0,1,0,1,1,1,1,0,0,0,0,1,3],
                [3,0,1,0,0,1,1,1,0,0,0,0,1,0,0,1,1,1,0,0,0,3],
                [3,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,3],
                [3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]
            ];

// x= 22
// y= 15




//-----------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------
                  // DRAW PLAYER

var player = new Object();
player.y = canvas.height/2-40;    //player position - middle of canvas - 40
player.x = canvas.width/2-40;     //player position - middle of canvas - 40
player.Width = 80;
player.Height = 80;
  player_image = new Image();
  player_image.src = 'http://sarahkerrigan.biz/wpmtest/1/images/horseright1.png';




function drawPlayer() {      // drawing the player
    context.beginPath();
    context.drawImage(player_image, player.x, player.y, player.Width, player.Height);
    context.closePath();
}
//-----------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------



      var updateX=(player.x-210);  // Starting point of canvas X
      var updateY=(player.y-160);  // Starting point of canvas Y
            var posX=updateX;
            var posY=updateY;




//------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------
       //DRAW THE MAP AND THE PLAYER      

function drawMap() {

var posY = updateY;    // new Y coordinates for the map after movement


var grass = new Image();
var stone = new Image();
var black = new Image();
            grass.src= 'http://sarahkerrigan.biz/wpmtest/1/images/tile/grass.jpeg';
            stone.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/sand.jpeg';
            black.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/black.png';

   //---------------------------------------------------------
                    // Draw the map loop
            grass.onload = function (){
            stone.onload = function (){
            black.onload = function (){
            for(var i=0; i < mapArray.length; i++){
                for(var j=0; j < mapArray[i].length; j++){


   //=======================================================================   
            //CHECK IF X AND Y POSITIONS OF THE TILE ARE WITHIN THE CANVAS
   //=======================================================================
                    if(mapArray[i][j]==0){
               if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX){
                        context.drawImage(grass,posX, posY, 64, 64);   // Load image for grass "0"
                           }
                    }



                    if(mapArray[i][j]==1){
               if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX){
                        context.drawImage(stone,posX,posY,64,64);     // Load image for stone "1"
                           }
                    }



                    if(mapArray[i][j]==3){
               if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX){
                        context.drawImage(black,posX,posY,64,64);     // Load image for black "3"
                           }
                    }
     //=======================================================================

                    posX+=64;
                }
                posY+=64;
                posX=updateX;   // new X coordinates for the map after movement
   //---------------------------------------------------------
              drawPlayer();          // Draw the player
            }
        }
     }
    }
}


//-----------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------

它看起来很简单,这就是为什么我想检查是否有任何你可以想到的东西可以更好地优化它。

以下是测试它的整个过程的链接:

https://jsfiddle.net/todorpet/cast5aq2/

此外,尽管将clearRect添加到画布周围的不可见部分作为图像。我也应该加上这个吗?

1 个答案:

答案 0 :(得分:1)

600,000fps不会发生。

代码中存在一个主要缺陷

第288行

setInterval(gameLoop, 30);        // 30 milisec to draw next frame

这会创建一个每30 ms调用一次的间隔事件。因为它位于函数gameLoop中,所以您只是创建了越来越多的间隔计时器。

在第一个调用游戏循环被调用〜每秒30次,在下一个循环中你添加另一个间隔,所以现在你每秒有~60个游戏循环调用,下一个调用你有90个,现在是额外的通话开始触发,对gameLoop的通话次数开始呈指数级增长。

如果gameLoop函数运行完美,那么在1000毫秒(1秒)内,您将有~20000个间隔,每个间隔创建~30次调用,即每秒约600000帧。

显然无法发生,浏览器会限制任何两个计时器事件之间的间隔,游戏循环功能运行的时间也会限制速率。

修复

您在代码'requestAnimationFrame`

中使用了它
function gameLoop(){
   playerMovement();          //Check for movements
   drawMap();                 //Draw the map and the player

   /* NEVER use setInterval or setTimeout for animating anything!!! */
   //setInterval(gameLoop, 30);        // 30 milisec to draw next frame

   // use this. It will automatically slow down the frame rate to 30frames 
   // 20, 15, 10 and so on, per second if your render code is slow.
   requestAnimationFrame(gameLoop);
}

到平铺地图

我找到你的功能。

function drawMap() {

    var posY = updateY; // new Y coordinates for the map after movement


    var grass = new Image();
    var stone = new Image();
    var black = new Image();
    grass.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/grass.jpeg';
    stone.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/sand.jpeg';
    black.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/black.png';

    grass.onload = function () {
        stone.onload = function () {
            black.onload = function () {
                for (var i = 0; i < mapArray.length; i++) {
                    for (var j = 0; j < mapArray[i].length; j++) {

                        if (mapArray[i][j] == 0) {
                            if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX) {
                                context.drawImage(grass, posX, posY, 64, 64); // Load image for grass "0"
                            }
                        }

                        if (mapArray[i][j] == 1) {
                            if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX) {
                                context.drawImage(stone, posX, posY, 64, 64); // Load image for stone "1"
                            }
                        }

                        if (mapArray[i][j] == 3) {
                            if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX) {
                                context.drawImage(black, posX, posY, 64, 64); // Load image for black "3"
                            }
                        }

                        posX += 64;
                    }
                    posY += 64;
                    posX = updateX; // new X coordinates for the map after movement
                    drawPlayer(); // Draw the player
                }
            }
        }
    }
}

这非常糟糕,您创建一组新图片的每一帧,然后您只将onload添加到第一张图片,当该图片加载时,您将onload事件添加到下一张图片,然后第三个相同。

这不仅非常缓慢且资源匮乏,它也可能无法随机工作。图像不会按照创建它们的顺序加载和触发onload事件,如果第二个图像在第一个图像加载之前加载onload事件没有设置,因此永远不会触发,第三个图像相同图像。

加载一次。

要为游戏使用资源,您可以在游戏开始之前加载所有游戏(加载屏幕)

当您使用平铺地图来引用图像时,最好将图像加载到索引的数组中以匹配地图。

const imageSrcDir = "http://sarahkerrigan.biz/wpmtest/1/images/tile/"
const tileImages = [];
function loadImages(images) {
    images.forEach(image => {
        const img = tileImages[image.mapIndex] = new Image();
        img.src = imageSrcDir + image.name;
    });
}
// load the images and add to the tileImage array
loadImages([
    { name : "grass.jpeg", mapIndex : 0 },
    { name : "stone.jpeg", mapIndex : 1 },
    { name : "black.png", mapIndex : 3 },
]);

播放器

您正在渲染循环的第二个循环中渲染播放器。这意味着你要渲染玩家15次。不好。你应该分开游戏的不同部分。绘制地图,然后将玩家作为单独的功能。

请参阅我如何处理播放器的示例。

设置地图。

首先展平地图,以便您可以快速访问它,我已将其转换为字符串,因此更容易编辑

const testMap = [
    "3333333333333333333333",
    "3000000000000000000003",
    "3000111000000011100003",
    "3011110000101111000013",
    "3010011100001001110003",
    "3000000000000000000003",
    "3000111000000011100003",
    "3011110000101111000013",
    "3010011100001001110003",
    "3000000000000000000003",
    "3000111000000011100003",
    "3011110000101111000013",
    "3010011100001001110003",
    "3000011000000001100003",
    "3333333333333333333333",
];

从上面的类型地图创建地图的功能。它也获得宽度和高度,并从字符串转换为数字。您可以使用任何字符来表示不同的地图位。

function createMap(map){
    const newMap = {};
    newMap.width = map[0].length;
    newMap.height = map.length;
    newMap.array = new Uint8Array(newMap.width * newMap.height);
    var index = 0;
    for(const row of map){
         var i = 0;
         while(i < row.length){
             newMap.array[index++] = Number(row[i++]);
         }
    }
    return newMap;
}
const currentMap = createMap(testMap);       

瓷砖

您需要有关瓷砖的一些信息

const tileWidth = 64;
const tileHeight = 64;

定位地图

使用展平的地图数据,您可以绘制地图,您需要有一个代表视图的地图位置(右上角)。您可以从玩家位置获取该位置,该位置应位于地图坐标中。

var playerX = 6; // in tile coordinates 6.5 would be halfway to till 7
var playerY = 2;

var mapX = 0;  // the map position so that the player can be seen
var mapY = 0;


// get the map position
function getMapPosition(){
    // convert player to pixel pos
    var x = playerX * tileWidth;
    var y = playerY * tileHeight; 
    x -= canvas.width / 2;     // center on the canvas
    y -= canvas.heigth / 2;       
    mapX = x;
    mapY = y;
}

绘制地图

有一些重要的部分。当您看到|0与楼层相同时。例如x = Math.floor(x)与x = x | 0x |= 0相同。这比地板快得多。

画布2D渲染器有一些必须避免的缺陷。当你绘制瓷砖时,你需要确保它们与画布像素对齐,否则你最终会在地图移动时在瓷砖之间闪烁接缝。

这在设置变换的行中得到修复。

ctx.setTransform(1,0,0,1,-mapX | 0,-mapY | 0);

mapXmapY被否定和搁置。地板将地图与像素对齐,确保没有接缝。

调用此函数后,画布变换将设置为地图坐标。然后,您可以在地图坐标处绘制所有其他游戏对象而不是画布坐标,从而使游戏中的绘图对象更加容易。

function drawMap(map) {
    const w = map.width; // get the width of the tile array
    const mArray = map.array;
    const tx = mapX / tileWidth | 0; // get the top left tile
    const ty = mapY / tileHeight | 0;
    const tW = (canvas.width / tileWidth | 0) + 2; // get the number of tiles to fit canvas
    const tH = (canvas.height / tileHeight | 0) + 2;
    // set the location via the transform
    // From here on you draw all the game items relative to the map not the canvas
    ctx.setTransform(1, 0, 0, 1, -mapX | 0, -mapY | 0);

    // Draw the tiles if tile pos is off map draw black tile
    for (var y = 0; y < tH; y += 1) {
        for (var x = 0; x < tW; x += 1) {
            const i = tx + x + (ty + y) * w;
            const tileIndex = mArray[i] === undefined ? 3 : mArray[i]; // if outside map draw black tile
            ctx.drawImage(tileImages[tileIndex], (tx + x) * tileWidth, (ty + y) * tileHeight);
        }
    }

}

这就是绘制平铺贴图的方法,也就是绘制平铺贴图的一种方法。

实施例

下面的代码段显示了它与你的角色放在一起,使用箭头键移动。

上面有一些细微的变化。

const ctx = canvas.getContext("2d");

const imageSrcDir = "http://sarahkerrigan.biz/wpmtest/1/images/tile/"
const tileImages = [];

requestAnimationFrame(mainLoop);  // start it after all code below has run
function mainLoop(){
  ctx.setTransform(1,0,0,1,0,0);
  
  //control the player
  if(keys.ArrowUp){
      player.y -= 0.1;
  }
  if(keys.ArrowDown){
      player.y += 0.1;
  }
  if(keys.ArrowLeft){
      player.x -= 0.1;
  }
  if(keys.ArrowRight){
      player.x += 0.1;
  }
  // Make sure the player stays on the mapo
  if(player.x < 2){ player.x = 2 }
  if(player.y < 2){ player.y = 2 }
  if(player.x >= currentMap.width-2){ player.x = currentMap.width-2}
  if(player.y >= currentMap.height-2){ player.y = currentMap.height-2}
  
  
  getMapPosition();
  drawMap(currentMap);
  player.draw();
  
  requestAnimationFrame(mainLoop);

}

function loadImages(images) {
  images.forEach(image => {
    const img = tileImages[image.mapIndex] = new Image();
    img.src = imageSrcDir + image.name;
  });
}
// load the images and add to the tileImage array
loadImages([{
    name: "grass.jpeg",
    mapIndex: 0
  },
  {
    name: "sand.jpeg",
    mapIndex: 1
  },
  {
    name: "black.png",
    mapIndex: 3
  },
]);

const player = {
  x: 6,
  y: 2,
  width: 80,
  height: 80,
  image: (() => {
    const img = new Image();
    img.src = "https://sarahkerrigan.biz/wpmtest/1/images/horseright1.png";
    return img;
  })(),
  draw(){
     ctx.drawImage(player.image,player.x * tileWidth - player.width / 2, player.y * tileHeight - player.height / 2);  
  },
};



const testMap = [
  "3333333333333333333333",
  "3000000000000000000003",
  "3000111000000011100003",
  "3011110000101111000013",
  "3010011100001001110003",
  "3000000000000000000003",
  "3000111000000011100003",
  "3011110000101111000013",
  "3010011100001001110003",
  "3000000000000000000003",
  "3000111000000011100003",
  "3011110000101111000013",
  "3010011100001001110003",
  "3000011000000001100003",
  "3333333333333333333333",
];

// function to create a map from the above type map 
function createMap(map) {
  const newMap = {};
  newMap.width = map[0].length;
  newMap.height = map.length;
  newMap.array = new Uint8Array(newMap.width * newMap.height);
  var index = 0;
  for (const row of map) {
    var i = 0;
    while (i < row.length) {
      newMap.array[index++] = Number(row[i++]);
    }
  }
  return newMap;
}
const currentMap = createMap(testMap);

const tileWidth = 64;
const tileHeight = 64;


var mapX = 0; // the map position so that the player can be seen
var mapY = 0;


// get the map position
function getMapPosition() {
  // convert player to pixel pos
  var x = player.x * tileWidth + player.width / 2;
  var y = player.y * tileHeight + player.height / 2;
  x -= canvas.width / 2; // center on the canvas
  y -= canvas.height / 2;
  mapX = x;
  mapY = y;
}




function drawMap(map) {
  const w = map.width; // get the width of the tile array
  const mArray = map.array;
  const tx = mapX / tileWidth | 0; // get the top left tile
  const ty = mapY / tileHeight | 0;
  const tW = (canvas.width / tileWidth | 0) + 2; // get the number of tiles to fit canvas
  const tH = (canvas.height / tileHeight | 0) + 2;
  // set the location via the transform
  // From here on you draw all the game items relative to the map not the canvas
  ctx.setTransform(1, 0, 0, 1, -mapX | 0, -mapY | 0);

  // Draw the tiles if tile pos is off map draw black tile
  for (var y = 0; y < tH; y += 1) {
    for (var x = 0; x < tW; x += 1) {
      const rx = tx + x;  // get tile real pos
      const ry = ty + y;
      var tileIndex;
      if(rx < 0 || rx >= w){
          tileIndex = 3; // black if off map
      }else{
          const i = rx + ry * w;
          tileIndex = mArray[i] === undefined ? 3 : mArray[i]; // if outside map draw black tile
      }
      ctx.drawImage(tileImages[tileIndex], rx * tileWidth, ry * tileHeight, tileWidth, tileHeight);
    }
  }

}


const keys = {
    ArrowUp : false,
    ArrowDown : false,
    ArrowLeft : false,
    ArrowRight : false,
};
function keyEvents(e){
    if(keys[e.code] !== undefined){
        keys[e.code] = e.type === "keydown";
        e.preventDefault();
    }
}
addEventListener("keyup", keyEvents);
addEventListener("keydown", keyEvents);
window.focus();
Arrow keys to move.
<canvas id="canvas" width="500" height="300"></canvas>