如何在js画布上仅绘制tilemap的可见部分?

时间:2017-10-30 08:51:03

标签: javascript canvas html5-canvas

我使用Tiled(3200 x 3200像素)创建了简单的tilemap。我使用this library

将其加载到画布上

每个seocnd我绘制整个tilemap 3200 x 3200 60次。 我试图四处走动,它工作正常。顺便说一下,我使用 ctx.translate 在画布上移动。我收录了这个in my own function

但是当我在Tiled(32000 x 32000像素)中创建更大的地图时 - 我得到了一个非常冻结的页面。我无法快速移动,我认为大概有10 fps

那么如何解决呢?我必须每秒调用 drawTiles()函数60次。但有没有办法只绘制瓷砖的可见部分?就像我只在屏幕上看到的那样(0,0,monitorWidth,monitorHeight我猜)

谢谢

1 个答案:

答案 0 :(得分:4)

绘制一个大的tileset

如果您有一个大的图块集并且只在画布中看到它的一部分,您只需要计算画布左上角的图块以及适合画布的横向和向下的图块数。

然后绘制适合画布的方形瓷砖阵列。

在示例中,图块集为1024 x 1024个图块(worldTileCount = 1024),每个图块为64 x 64像素tileSize = 64,使总游戏区域为65536像素正方形

左上方图块的位置由变量worldXworldY

设置

绘制图块的功能

// val | 0 is the same as Math.floor(val)

var worldX = 512 * tileSize;  // pixel position of playfield
var worldY = 512 * tileSize;

function drawWorld(){
  const c = worldTileCount; // get the width of the tile array
  const s = tileSize;       // get the tile size in pixels

  // get the tile position
  const tx = worldX / s | 0;  // get the top left tile
  const ty = worldY / s | 0;

  // get the number of tiles that will fit the canvas
  const tW = (canvas.width / s | 0) + 2;  
  const tH = (canvas.height / s | 0) + 2;

  // set the location. Must floor to pixel boundary or you get holes
  ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);  

  // Draw the tiles across and down
  for(var y = 0; y < tH; y += 1){
     for(var x = 0; x < tW; x += 1){
         // get the index into the tile array for the tile at x,y plus the topleft tile
         const i = tx + x + (ty + y) * c;

         // get the tile id from the tileMap. If outside map default to tile 6
         const tindx = tileMap[i] === undefined ? 6 : tileMap[i];

         // draw the tile at its location. last 2 args are x,y pixel location
         imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
     }
  }

}

setTransform和绝对坐标。

使用绝对坐标可以使一切变得简单。

使用画布上下文setTransform设置世界位置,然后可以在自己的坐标处绘制每个图块。

   // set the world location. The | 0 floors the values and ensures no holes
   ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);  

这样,如果你在51023,34256位置有一个角色,你可以在那个位置画出它。

   playerX = 51023;
   playerY = 34256;
   ctx.drawImage(myPlayerImage,playerX,playerY);

如果你想要相对于玩家的瓷砖地图,那么只需将世界位置设置为画布尺寸的一半,然后设置为左边加一个图块以确保重叠

   playerX = 51023;
   playerY = 34256;

   worldX = playerX - canvas.width / 2 - tileWidth;
   worldY = playerY - canvas.height / 2 - tileHeight;

通过65536像素图块地图演示大65536。

如果您拥有马匹并且能够处理更大的速度而没有任何帧速率损失,则以60fps。 (使用此方法的地图大小限制大约为4,000,000,000乘4,000,000,000像素(32位整数坐标))

更新15/5/2019重新抖动

评论指出地图滚动时会出现一些抖动

我做了一些改动,以平滑的方式平滑掉随机路径,每240帧翻一番(4秒,60fps)另外还添加了一个帧率降低器,如果你在画布上单击并按住鼠标按钮的帧速率将减慢到正常的1/8,以便更容易看到抖动

抖动有两个原因。

时间错误

第一个和最少的是requestAnimationFrame传递给更新函数的时间,间隔不完美,由于时间而导致的舍入误差加剧了对齐问题。

为减少时间误差,我将移动速度设置为恒定间隔,以最大限度地减少帧之间的舍入误差漂移。

将图块与像素对齐

抖动的主要原因是必须在像素边界上渲染切片。如果没有,则别名错误将在切片之间创建可见的接缝。

要查看差异,请单击左上角的按钮以打开和关闭像素对齐。

要获得平滑滚动(子像素定位),请将地图绘制到与像素对齐的屏幕外画布,然后将该画布渲染到显示画布,添加子像素偏移。这将使用画布提供最佳结果。为了更好,您将需要使用webGL

更新结束

var refereshSkip = false; // when true drops frame rate by 4
var dontAlignToPixel = false;
var ctx = canvas.getContext("2d");
function mouseEvent(e) {
   if(e.type === "click") {
       dontAlignToPixel = !dontAlignToPixel;
       pixAlignInfo.textContent = dontAlignToPixel ? "Pixel Align is OFF" : "Pixel Align is ON";
   } else {
       refereshSkip = e.type === "mousedown";
   }
}
pixAlignInfo.addEventListener("click",mouseEvent);
canvas.addEventListener("mousedown",mouseEvent);
canvas.addEventListener("mouseup",mouseEvent);


// wait for code under this to setup
setTimeout(() => {


  var w = canvas.width;
  var h = canvas.height;
  var cw = w / 2; // center 
  var ch = h / 2;



  // create tile map
  const worldTileCount = 1024;
  const tileMap = new Uint8Array(worldTileCount * worldTileCount);
  
  // add random tiles
  doFor(worldTileCount * worldTileCount, i => {
    tileMap[i] = randI(1, tileCount);
  });
  
  // this is the movement direction of the map
  var worldDir = Math.PI / 4;



/* =======================================================================
   Drawing the tileMap 
========================================================================*/


  var worldX = 512 * tileSize;
  var worldY = 512 * tileSize;

  function drawWorld() {
    const c = worldTileCount; // get the width of the tile array
    const s = tileSize; // get the tile size in pixels
    const tx = worldX / s | 0; // get the top left tile
    const ty = worldY / s | 0;
    const tW = (canvas.width / s | 0) + 2; // get the number of tiles to fit canvas
    const tH = (canvas.height / s | 0) + 2;
    // set the location
    if(dontAlignToPixel) {
        ctx.setTransform(1, 0, 0, 1, -worldX,-worldY);
        
    } else {
        ctx.setTransform(1, 0, 0, 1, Math.floor(-worldX),Math.floor(-worldY));
    }
    // Draw the tiles
    for (var y = 0; y < tH; y += 1) {
      for (var x = 0; x < tW; x += 1) {
        const i = tx + x + (ty + y) * c;
        const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
        imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
      }
    }

  }
  var timer = 0;
  var refreshFrames = 0;
  const dirChangeMax = 3.5;
  const framesBetweenDirChange = 240;
  var dirChangeDelay = 1;
  var dirChange = 0;
  var prevDir = worldDir;
  const eCurve   = (v, p = 2) =>  v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p); 
 
  //==============================================================
  // main render function
  function update() {
    refreshFrames ++;
    if(!refereshSkip || (refereshSkip && refreshFrames % 8 === 0)){
      timer += 1000 / 60;
      ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
      ctx.globalAlpha = 1; // reset alpha
      if (w !== innerWidth || h !== innerHeight) {
        cw = (w = canvas.width = innerWidth) / 2;
        ch = (h = canvas.height = innerHeight) / 2;
      } else {
        ctx.clearRect(0, 0, w, h);
      }
    
      // Move the map
      var speed = Math.sin(timer / 10000) * 8;
      worldX += Math.cos(worldDir) * speed;
      worldY += Math.sin(worldDir) * speed;
      if(dirChangeDelay-- <= 0) {
        dirChangeDelay = framesBetweenDirChange;
        prevDir = worldDir = prevDir + dirChange;
        dirChange = rand(-dirChangeMax , dirChangeMax);

      }
      worldDir = prevDir + (1-eCurve(dirChangeDelay / framesBetweenDirChange,3)) * dirChange;
    
      // Draw the map
      drawWorld();
    }
    requestAnimationFrame(update);
  }
  requestAnimationFrame(update);
}, 0);




/*===========================================================================
  CODE FROM HERE DOWN UNRELATED TO THE ANSWER
  
  ===========================================================================*/






const imageTools = (function() {
  // This interface is as is. No warenties no garenties, and NOT to be used comercialy
  var workImg, workImg1, keep; // for internal use
  keep = false;
  var tools = {
    canvas(width, height) { // create a blank image (canvas)
      var c = document.createElement("canvas");
      c.width = width;
      c.height = height;
      return c;
    },
    createImage: function(width, height) {
      var i = this.canvas(width, height);
      i.ctx = i.getContext("2d");
      return i;
    },
    drawSpriteQuick: function(image, spriteIndex, x, y) {
      var w, h, spr;
      spr = image.sprites[spriteIndex];
      w = spr.w;
      h = spr.h;
      ctx.drawImage(image, spr.x, spr.y, w, h, x, y, w, h);
    },
    line(x1, y1, x2, y2) {
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
    },
    circle(x, y, r) {
      ctx.moveTo(x + r, y);
      ctx.arc(x, y, r, 0, Math.PI * 2);
    },
  };
  return tools;
})();

const doFor = (count, cb) => {
  var i = 0;
  while (i < count && cb(i++) !== true);
}; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const seededRandom = (() => {
  var seed = 1;
  return {
    max: 2576436549074795,
    reseed(s) {
      seed = s
    },
    random() {
      return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
    }
  }
})();
const randSeed = (seed) => seededRandom.reseed(seed | 0);
const randSI = (min, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const tileSize = 64;
const tileCount = 7;

function drawGrass(ctx, c1, c2, c3) {
  const s = tileSize;
  const gs = s / (8 * c3);
  ctx.fillStyle = c1;
  ctx.fillRect(0, 0, s, s);

  ctx.strokeStyle = c2;
  ctx.lineWidth = 2;
  ctx.lineCap = "round";
  ctx.beginPath();
  doFor(s, i => {
    const x = rand(-gs, s + gs);
    const y = rand(-gs, s + gs);
    const x1 = rand(x - gs, x + gs);
    const y1 = rand(y - gs, y + gs);
    imageTools.line(x, y, x1, y1);
    imageTools.line(x + s, y, x1 + s, y1);
    imageTools.line(x - s, y, x1 - s, y1);
    imageTools.line(x, y + s, x1, y1 + s);
    imageTools.line(x, y - s, x1, y1 - s);
  })
  ctx.stroke();
}

function drawTree(ctx, c1, c2, c3) {

  const seed = Date.now();
  const s = tileSize;
  const gs = s / 2;
  const gh = gs / 2;
  ctx.fillStyle = c1;
  ctx.strokeStyle = "#000";
  ctx.lineWidth = 2;
  ctx.save();
  ctx.shadowColor = "rgba(0,0,0,0.5)";
  ctx.shadowBlur = 4;
  ctx.shadowOffsetX = 8;
  ctx.shadowOffsetY = 8;
  randSeed(seed);
  ctx.beginPath();
  doFor(18, i => {
    const ss = 1 - i / 18;
    imageTools.circle(randS(gs - gh * ss, gs + gh * ss), randS(gs - gh * ss, gs + gh * ss), randS(gh / 4, gh / 2));
  })
  ctx.stroke();
  ctx.fill();
  ctx.restore();
  ctx.fillStyle = c2;
  ctx.strokeStyle = c3;
  ctx.lineWidth = 2;
  ctx.save();

  randSeed(seed);
  ctx.beginPath();
  doFor(18, i => {
    const ss = 1 - i / 18;
    imageTools.circle(randS(gs - gh * ss, gs + gh * ss) - 2, randS(gs - gh * ss, gs + gh * ss) - 2, randS(gh / 4, gh / 2) / 1.6);
  })
  ctx.stroke();
  ctx.fill();
  ctx.restore();


}


const tileRenders = [
  (ctx) => {
    drawGrass(ctx, "#4C4", "#4F4", 1)
  },
  (ctx) => {
    drawGrass(ctx, "#644", "#844", 2)
  },
  (ctx) => {
    tileRenders[0](ctx);
    drawTree(ctx, "#480", "#8E0", "#7C0")
  },
  (ctx) => {
    tileRenders[1](ctx);
    drawTree(ctx, "#680", "#AE0", "#8C0")
  },
  (ctx) => {
    drawGrass(ctx, "#008", "#00A", 4)
  },
  (ctx) => {
    drawGrass(ctx, "#009", "#00C", 4)
  },
  (ctx) => {
    drawGrass(ctx, "#00B", "#00D", 4)
  },
]
const tileSet = imageTools.createImage(tileSize * tileCount, tileSize);
const ctxMain = ctx;
ctx = tileSet.ctx;
tileSet.sprites = [];
doFor(tileCount, i => {
  x = i * tileSize;
  ctx.save();
  ctx.setTransform(1, 0, 0, 1, x, 0);
  ctx.beginPath();
  ctx.rect(0, 0, tileSize, tileSize);
  ctx.clip()
  if (tileRenders[i]) {
    tileRenders[i](ctx)
  }
  tileSet.sprites.push({
    x,
    y: 0,
    w: tileSize,
    h: tileSize
  });
  ctx.restore();
});
ctx = ctxMain;
canvas {
  position: absolute;
  top: 0px;
  left: 0px;
}
div {
  position: absolute;
  top: 8px;
  left: 8px;
  color: white;
}
#pixAlignInfo {
  color: yellow;
  cursor: pointer;
  border: 2px solid green;
  margin: 4px;
}
#pixAlignInfo:hover {
  color: white;
  background: #0008;
}
body {
  background: #49c;
}
<canvas id="canvas"></canvas>
<div>Hold left button to slow to 1/8th<br>
  <span id="pixAlignInfo">Click toggle pixel alignment. Alignment is ON</span></div>