【问题标题】:Load and draw tiled images to canvas in JavaScript在 JavaScript 中加载平铺图像并将其绘制到画布
【发布时间】:2015-08-23 04:23:55
【问题描述】:

我似乎很清楚如何使用 JavaScript 动态加载和绘制图像。我附上一个onload函数并设置图片来源:

var img = new Image();
img.onload = function() {
    ctx.drawImage(img, 0, 0);
}
img.src = "your_img_path";

我想对二维图像数组(图块)执行此操作,并多次更新这些图块。但是,如后所述,我在实现这一点时遇到了问题。

我编写的函数(如下所示)包含一些与我当前的问题无关的计算。它们的目的是允许平移/缩放图块(模拟相机)。选择加载的图块取决于请求渲染的位置。

setInterval(redraw, 35);

function redraw()
{
    var canvas = document.getElementById("MyCanvas");
    var ctx = canvas.getContext("2d");

    //Make canvas full screen.
    canvas.width  = window.innerWidth;
    canvas.height = window.innerHeight;

    ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);

    //Compute the range of in-game coordinates.
    var lowestX = currentX - (window.innerWidth / 2) / zoom;
    var highestX = currentX + (window.innerWidth / 2) / zoom;
    var lowestY = currentY - (window.innerHeight / 2) / zoom;
    var highestY = currentY + (window.innerHeight / 2) / zoom;

    //Compute the amount of tiles that can be displayed in each direction.
    var imagesX = Math.ceil((highestX - lowestX) / imageSize);
    var imagesY = Math.ceil((highestY - lowestY) / imageSize);

    //Compute viewport offsets for the grid of tiles.
    var offsetX = -(mod(currentX, imageSize) * mapZoom);
    var offsetY = -(mod(currentY, imageSize) * mapZoom);

    //Create array to store 2D grid of tiles and iterate through.
    var imgArray = new Array(imagesY * imagesX);
    for (var yIndex = 0; yIndex < imagesY; yIndex++) {
        for (var xIndex = 0; xIndex < imagesX; xIndex++) {
            //Determine name of image to load.
            var iLocX = (lowestX + (offsetX / mapZoom) + (xIndex * imageSize));
            var iLocY = (lowestY + (offsetY / mapZoom) + (yIndex * imageSize));
            var iNameX = Math.floor(iLocX / imageSize);
            var iNameY = Math.floor(iLocY / imageSize);

            //Construct image name.
            var imageName = "Game/Tiles_" + iNameX + "x" + iNameY + ".png";

            //Determine the viewport coordinates of the images.
            var viewX = offsetX + (xIndex * imageSize * zoom);
            var viewY = offsetY + (yIndex * imageSize * zoom);

            //Create Image
            imgArray[yIndex * imagesX + xIndex] = new Image();
            imgArray[yIndex * imagesX + xIndex].onload = function() {
                ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
            };
            imgArray[yIndex * imagesX + xIndex].src = imageName;
        }   
    }    
}

如您所见,函数中声明了一个数组,以等于显示的瓷砖网格的大小。数组的元素表面上填充了动态加载后绘制的图像。不幸的是,imgArray[yIndex * imagesX + xIndex] 不被我的浏览器视为图像,我收到错误:

未捕获的类型错误:无法在“CanvasRenderingContext2D”上执行“drawImage”:提供的值不是类型“(HTMLImageElement 或 HTMLVideoElement 或 HTMLCanvasElement 或 ImageBitmap)”

另外,我最初认为数组没有必要。毕竟,如果我只是要在下次调用redraw 时加载一个新图像,为什么我需要在完成绘制后保留图像的句柄?在这种情况下,循环如下所示:

for (var yIndex = 0; yIndex < imagesY; yIndex++) {
    for (var xIndex = 0; xIndex < imagesX; xIndex++) {
        //Determine name of image to load.
        var iLocX = (lowestX + (offsetX / mapZoom) + (xIndex * imageSize));
        var iLocY = (lowestY + (offsetY / mapZoom) + (yIndex * imageSize));
        var iNameX = Math.floor(iLocX / imageSize);
        var iNameY = Math.floor(iLocY / imageSize);

        //Construct image name.
        var imageName = "Game/Tiles_" + iNameX + "x" + iNameY + ".png";

        //Determine the viewport coordinates of the images.
        var viewX = offsetX + (xIndex * imageSize * zoom);
        var viewY = offsetY + (yIndex * imageSize * zoom);

        //Create Image
        var img = new Image();
        img.onload = function() {
            ctx.drawImage(img, viewX, viewY, imageSize * zoom, imageSize * zoom);
        };
        img.src = imageName;
    }   
}

自从最后一个处理的图块被渲染后,我对这种方法的运气更好。尽管如此,没有绘制所有其他瓷砖。我想我可能一直在覆盖一些东西,所以只有最后一个保留了它的值。

  1. 那么,我该怎么做才能重复渲染 2D 格子网格,从而始终加载瓷砖?

  2. 为什么数组元素不被视为图像?

【问题讨论】:

  • imgArray[yIndex * imagesX + xIndex]的类型是什么?尝试将元素记录到控制台。还有为什么是imgArray[xIndex].src = imageName; 而不是yIndex * imagesX + xIndex
  • 索引不匹配是输入问题时的拼写错误,我会修复它。至于类型,我认为应该是Image,因为这就是所谓的。我通过控制台记录了图像并打印了&lt;img src="Game/Tiles_1x0.png"&gt;

标签: javascript image canvas game-engine


【解决方案1】:

您犯了所有 JavaScript 程序员有时都会犯的经典错误。问题出在这里:

imgArray[yIndex * imagesX + xIndex].onload = function() {
    ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};

您在循环中定义一个函数,并期望每个函数都有自己的变量副本。这不是发生的事情。您定义的每个函数都使用相同的变量yIndexxIndexviewXviewY。请注意,函数使用这些变量,而不是这些变量的值。

这很重要:您在循环中定义的函数在定义函数时不使用yIndex(和xIndexviewXviewY)的值。函数本身使用变量。每个函数在执行函数时都会计算 yIndex(以及 xIndex 和其他函数)。

我们说这些变量已经绑定在closure 中。当循环结束时,yIndexxIndex 超出范围,因此函数计算超出数组末尾的位置。

解决方案是编写另一个函数,将yIndexxIndexviewXviewY 传递给该函数,并创建一个新函数来捕获您要使用的值。这些值将存储在与循环中的变量分开的新变量中。

你可以把这个函数定义放在redraw()的循环之前:

function makeImageFunction(yIndex, xIndex, viewX, viewY) {
    return function () {
        ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
    };
}

现在将有问题的行替换为对函数生成函数的调用:

imgArray[yIndex * imagesX + xIndex].onload = makeImageFunction(yIndex, xIndex, viewX, viewY);

另外,请确保您设置了正确图像的来源:

imgArray[yIndex * imagesX + xIndex].src = imageName;

为了使您的代码更易于阅读和维护,请排除yIndex * imagesX + xIndex 的重复计算。最好计算一次图像索引,然后到处使用。

您可以像这样简化makeImageFunction

function makeImageFunction(index, viewX, viewY) {
    return function () {
        ctx.drawImage(imgArray[index], viewX, viewY, imageSize * zoom, imageSize * zoom);
    };
}

然后在循环中写这个:

// Create image.
var index = yIndex * imagesX + xIndex;
imgArray[index] = new Image();
imgArray[index].onload = makeImageFunction(index, viewX, viewY);
imgArray[index].src = imageName;

您也可能希望排除imgArray[index]

// Create image.
var index = yIndex * imagesX + xIndex;
var image = imgArray[index] = new Image();
image.onload = makeImageFunction(index, viewX, viewY);
image.src = imageName;

在这一点上,我们问自己为什么我们有一个图像数组。如果,正如您在问题中所说,一旦您加载了图像,您就不再使用数组,我们可以完全摆脱它。

现在我们传递给makeImageFunction 的参数是图像:

function makeImageFunction(image, viewX, viewY) {
    return function () {
        ctx.drawImage(image, viewX, viewY, imageSize * zoom, imageSize * zoom);
    };
}

在循环内部我们有:

// Create image.
var image = new Image();
image.onload = makeImageFunction(image, viewX, viewY);
image.src = imageName;

这看起来类似于您在问题中提到的替代方案,只是它在循环内没有函数定义。相反,它调用一个函数生成函数,该函数在一个新函数中捕获imageviewXviewY 的当前值。

现在您可以弄清楚为什么您的原始代码只绘制了最后一张图片。您的循环定义了一大堆函数,它们都引用了变量image。当循环结束时,image 的值是您制作的最后一张图片,因此所有函数都引用了最后一张图片。因此,他们都绘制了相同的图像。他们把它画在同一个位置,因为他们都使用了相同的变量viewXviewY

【讨论】:

  • 太棒了! JavaScript 是一种很奇怪的语言。从来没有听说过函数闭包——谢天谢地,这解决了问题!
  • 虽然您的回答是可以接受的,但我仍然需要查看 Blindman67 的回答。似乎他的解决方案通过不同的实现完全避开了我遇到的问题。到目前为止,很难确定哪个答案“更好”。无论如何,两者或您的答案都可能会保持在顶部附近。我真的很高兴你发布了这个,它非常有用,并且减轻了很多心痛。
  • 我承认。这个问题与您的答案更相关,因为这仅与图像加载有关。我非常感谢所有的帮助。
【解决方案2】:

Michael Laszlo 提供了一个很好的答案,但我想在这种情况下添加它,因为这是关于具有特殊考虑的图形应用程序。

更新画布的内容时,您应该将其全部保存在一个函数中。当所有其他页面布局和渲染完成后,应该调用该函数。每秒调用此函数的次数不应超过 60 次。以 ad hoc 方式访问画布很麻烦,并且会减慢整个页面的速度。

浏览器提供了一种方法来确保画布重绘的时间与其自己的布局和渲染同步。 window.requestAnimationFrame (renderFunction);https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame

没有必要给每张图片都加上onload事件,太浪费资源了。您只需将图像添加到数组中,然后在每个动画帧上通过检查每个图像的 complete 属性来检查数组中的图像是否已加载。

我假设图块是背景的一部分,因此只需要渲染一次。最好创建一个页外画布对象并向其渲染图块,然后将该画布渲染到用户看到的页上画布。

... // all in scope of your game object
// create a background canvas and get the 2d context
function setup(){
    // populate your imageArray with the image tiles

    // Now call requestAmimationFrame
    window.requestAnimationFrame(myRenderTileSetup);
}
function myRenderTileSetup(){ // this renders the tiles as they are avalible
    var renderedCount = 0;
    for(y = 0; y < tilesHeight; y+=1 ){
        for(x = 0; x < tilesWidth; x+=1 ){
            arrayIndex = x+y*tilesWidth;
            // check if the image is available and loaded.
            if(imageArray[arrayIndex] && imageArray[arrayIndex].complete){
                 backGroundCTX.drawImage(imageArray[arrayIndex],... // draw the image
                 imageArray[arrayIndex] = undefined; // we dont need the image
                                                     // so unreference it
            }else{  // the image is not there so must have been rendered
                 renderedCount += 1;  // count rendered tiles.
            }
        }
     }
     // when all tiles have been loaded and rendered switch to the game render function
     if(renderedCount === tilesWidth*tilesHeight){
          // background completely rendered
          // switch rendering to the main game render.
          window.requestAnimationFrame(myRenderMain);
     }else{
          // if not all available keep doing this till all tiles complete/ 
          window.requestAnimationFrame(myRenderTileSetup);
     }
}

function myRenderMain(){
    ... // render the background tiles canvas

    ... // render game graphics.

    // now ready for next frame.
    window.requestAnimationFrame(myRenderMain);
}

这只是一个展示图块渲染的示例,以及我如何想象您的游戏可以运行而不是用作功能代码。此方法简化了渲染过程,并且仅在需要时执行。它还通过不以 add hock (Image.onload) 方式调用绘图函数来将 GPU(图形处理单元)状态更改保持在最低限度。

它还不需要为每个平铺图像添加 onload 函数,这本身就是额外的开销。如果像 Michael Laszlo 建议的那样为每个图像添加一个新函数,如果不删除图像引用或 onload 属性引用的函数,则可能存在内存泄漏的风险。最好尽量减少与 DOM 和 DOM 对象(例如 HTMLImageElement)的所有交互,如果不需要,请始终确保取消引用它们,以免它们浪费内存。

如果浏览器窗口不可见,requestAnimationFrame 也会停止调用,从而允许设备上的其他进程获得更多 CPU 时间,从而更加用户友好。

编写游戏需要密切关注性能,每一点都有帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-04
    • 1970-01-01
    • 2014-04-10
    • 1970-01-01
    • 1970-01-01
    • 2022-01-01
    • 1970-01-01
    • 2015-03-10
    相关资源
    最近更新 更多