优化简单的2D平铺引擎(+潜在错误修复)

时间:2013-04-18 09:50:35

标签: c++ optimization graphics 2d tile-engine

前言

是的,这里有很多内容......但是我会尽我所能保持这个有条理,内容丰富且直截了当的意思!

在C ++中使用HGE library,我创建了一个简单的tile引擎 到目前为止,我已经实现了以下设计:

  • 一个CTile类,表示CTileLayer中的单个图块,包含行/列信息以及HGE::hgeQuad(存储顶点,颜色和纹理信息,{{3}有关详情)。
  • 一个CTileLayer类,表示瓷砖的二维“平面”(存储为CTile个对象的一维数组),包含行数/列数X / Y世界坐标信息,平铺像素宽度/高度信息以及图层的整体宽度/高度(以像素为单位)。

CTileLayer负责渲染在虚拟摄像机“视口”边界内完全或部分可见的任何图块,并避免对此可见范围之外的任何图块执行此操作。在创建时,它预先计算要存储在每个CTile对象中的所有信息,因此引擎的核心有更多的呼吸空间,并且可以严格关注渲染循环。当然,它还处理每个包含的瓷砖的正确释放。

<小时/>

问题

我现在面临的问题基本上归结为以下架构/优化问题:

  1. 在我的渲染循环中,即使我没有渲染任何超出可见范围的图块,我仍然 循环 通过所有图块,这似乎对较大的tilemaps产生重大性能影响(即,任何高于100x100行/列的东西@ 64x64 tile尺寸仍会使帧速率下降50%或更多)。
  2. 最终,我打算创建一个花哨的tilemap编辑器,以配合这个引擎 但是,由于我将所有二维信息存储在一个或多个1D阵列中,我不知道如何实现某种矩形选择&amp;复制/粘贴功能,没有一些 MAJOR 性能命中 - 涉及每帧两次循环每个图块。 然而,如果我使用2D数组,会有一个稍微但更普遍的FPS下降!
  3. 错误

    如前所述......在我的CTileLayer对象的渲染代码中,我根据它们是否在观察范围内来优化要绘制的图块。这很好用,对于较大的地图,我注意到只有3-8 FPS下降(相比之下没有这个优化的100+ FPS下降)。

    但是我认为我正在错误地计算这个范围,因为在地图中间滚动之后你可以开始看到一个间隙(在最上面和最左边)没有渲染图块,好像剪裁范围增加得更快相机可以移动(即使它们都以相同的速度移动)。

    这个间隙的尺寸逐渐增大,进一步增加到X&amp; Y轴你走了,最终吃了近一半的顶部&amp;屏幕左侧是大地图。 我的渲染代码如下所示......

    <小时/>

    代码

    //
    // [Allocate]
    // For pre-calculating tile information
    // - Rows/Columns   = Map Dimensions (in tiles)
    // - Width/Height   = Tile Dimensions (in pixels)
    //
    void CTileLayer::Allocate(UINT numColumns, UINT numRows, float tileWidth, float tileHeight)
    {
        m_nColumns = numColumns;
        m_nRows = numRows;
    
        float x, y;
        UINT column = 0, row = 0;
        const ULONG nTiles = m_nColumns * m_nRows;
        hgeQuad quad;
    
        m_tileWidth = tileWidth;
        m_tileHeight = tileHeight;
        m_layerWidth = m_tileWidth * m_nColumns;
        m_layerHeight = m_tileHeight * m_nRows;
    
        if(m_tiles != NULL) Free();
        m_tiles = new CTile[nTiles];
    
        for(ULONG l = 0; l < nTiles; l++)
        {
            m_tiles[l] = CTile();
            m_tiles[l].column = column;
            m_tiles[l].row = row;
            x = (float(column) * m_tileWidth) + m_offsetX;
            y = (float(row) * m_tileHeight) + m_offsetY;
    
            quad.blend = BLEND_ALPHAADD | BLEND_COLORMUL | BLEND_ZWRITE;
            quad.tex = HTEXTURE(nullptr); //Replaced for the sake of brevity (in the engine's code, I used a globally allocated texture array and did some random tile generation here)
    
            for(UINT i = 0; i < 4; i++)
            {
                quad.v[i].z = 0.5f;
                quad.v[i].col = 0xFF7F7F7F;
            }
            quad.v[0].x = x;
            quad.v[0].y = y;
            quad.v[0].tx = 0;
            quad.v[0].ty = 0;
            quad.v[1].x = x + m_tileWidth;
            quad.v[1].y = y;
            quad.v[1].tx = 1.0;
            quad.v[1].ty = 0;
            quad.v[2].x = x + m_tileWidth;
            quad.v[2].y = y + m_tileHeight;
            quad.v[2].tx = 1.0;
            quad.v[2].ty = 1.0;
            quad.v[3].x = x;
            quad.v[3].y = y + m_tileHeight;
            quad.v[3].tx = 0;
            quad.v[3].ty = 1.0;
            memcpy(&m_tiles[l].quad, &quad, sizeof(hgeQuad));
    
            if(++column > m_nColumns - 1) {
                column = 0;
                row++;
            }
        }
    }
    
    //
    // [Render]
    // For drawing the entire tile layer
    // - X/Y          = world position
    // - Top/Left     = screen 'clipping' position
    // - Width/Height = screen 'clipping' dimensions
    //
    bool CTileLayer::Render(HGE* hge, float cameraX, float cameraY, float cameraTop, float cameraLeft, float cameraWidth, float cameraHeight)
    {
        // Calculate the current number of tiles
        const ULONG nTiles = m_nColumns * m_nRows;
    
        // Calculate min & max X/Y world pixel coordinates
        const float scalarX = cameraX / m_layerWidth;  // This is how far (from 0 to 1, in world coordinates) along the X-axis we are within the layer
        const float scalarY = cameraY / m_layerHeight; // This is how far (from 0 to 1, in world coordinates) along the Y-axis we are within the layer
        const float minX = cameraTop + (scalarX * float(m_nColumns) - m_tileWidth); // Leftmost pixel coordinate within the world
        const float minY = cameraLeft + (scalarY * float(m_nRows) - m_tileHeight);  // Topmost pixel coordinate within the world
        const float maxX = minX + cameraWidth + m_tileWidth;                        // Rightmost pixel coordinate within the world
        const float maxY = minY + cameraHeight + m_tileHeight;                      // Bottommost pixel coordinate within the world
    
        // Loop through all tiles in the map
        for(ULONG l = 0; l < nTiles; l++)
        {
            CTile tile = m_tiles[l];
            // Calculate this tile's X/Y world pixel coordinates
            float tileX = (float(tile.column) * m_tileWidth) - cameraX;
            float tileY = (float(tile.row) * m_tileHeight) - cameraY;
    
            // Check if this tile is within the boundaries of the current camera view
            if(tileX > minX && tileY > minY && tileX < maxX && tileY < maxY) {
                // It is, so draw it!
                hge->Gfx_RenderQuad(&tile.quad, -cameraX, -cameraY);
            }
        }
    
        return false;
    }
    
    //
    // [Free]
    // Gee, I wonder what this does? lol...
    //
    void CTileLayer::Free()
    {
        delete [] m_tiles;
        m_tiles = NULL;
    }
    


    <小时/>

    问题

    1. 如何解决这些体系结构/优化问题,而不会对任何其他渲染优化产生太大影响?
    2. 为什么会发生这个错误?怎么修好?
    3. <小时/> 感谢您的时间!

2 个答案:

答案 0 :(得分:1)

优化迭代地图非常简单。

如果在世界坐标(左,上,右,下)中有一个可见的矩形,那么通过除以瓷砖大小来计算出瓷砖位置是相当简单的。

一旦你有了这些瓷砖坐标(tl,tt,tr,tb),你就可以很容易地计算出1D阵列中的第一个可见瓷砖。 (从2D坐标计算任何tile索引的方式是(y * width)+ x - 请记住确保输入坐标首先有效。)然后你只需要一个double for循环来迭代可见的tile:

int visiblewidth = tr - tl + 1;
int visibleheight = tb - tt + 1;

for( int rowidx = ( tt * layerwidth ) + tl; visibleheight--; rowidx += layerwidth )
{
    for( int tileidx = rowidx, cx = visiblewidth; cx--; tileidx++ )
    {
        // render m_Tiles[ tileidx ]...
    }
}

您可以使用类似的系统来选择一块瓷砖。只需存储选择坐标并以完全相同的方式计算实际图块。

至于你的错误,为什么你有相机的x,y,左,右,宽,高?只需存储摄像机位置(x,y)并根据屏幕/视口的尺寸以及您定义的任何缩放系数计算可见矩形。

答案 1 :(得分:0)

这是伪编码示例,几何变量在2d向量中。相机对象和平铺图都具有中心位置和范围(半尺寸)。即使你决定坚持使用纯数字,数学也是一样的。即使你不使用中心坐标和范围,也许你会对数学有所了解。所有这些代码都在render函数中,并且相当简单。此外,此示例假设您已经拥有一个类似2D数组的对象来保存切片。

所以,首先是一个完整的例子,我将进一步向下解释每个部分。

// x and y are counters, sx is a placeholder for x start value as x will 
// be in the inner loop and need to be reset each iteration.
// mx and my will be the values x and y will count towards too.
x=0,
y=0, 
sx=0, 
mx=total_number_of_tiles_on_x_axis, 
my=total_number_of_tiles_on_y_axis

// calculate the lowest and highest worldspace values of the cam
min = cam.center - cam.extent
max = cam.center + cam.extent

// subtract with tilemap corners and divide by tilesize to get 
// the anount of tiles that is outside of the cameras scoop
floor = Math.floor( min - ( tilemap.center - tilemap.extent ) / tilesize)
ceil = Math.ceil( max - ( tilemap.center + tilemap.extent ) / tilesize)

if(floor.x > 0)
    sx+=floor.x
if(floor.y > 0)
    y+=floor.y

if(ceil.x < 0)
    mx+=ceil.x
if(ceil.y < 0)
    my+=ceil.y

for(; y<my; y++)
    // x need to be reset each y iteration, start value are stored in sx
    for(x=sx; x<mx; x++)
       // render tile x in tilelayer y

一点一点地解释。在render函数中,我们将使用一些变量。

// x and y are counters, sx is a placeholder for x start value as x will 
// be in the inner loop and need to be reset each iteration.
// mx and my will be the values x and y will count towards too.
x=0,
y=0, 
sx=0, 
mx=total_number_of_tiles_on_x_axis, 
my=total_number_of_tiles_on_y_axis

要防止渲染所有图块,您需要提供类似相机的对象或有关可见区域开始和停止位置的信息(如果场景可移动,则在世界空间中)

在这个例子中,我向渲染函数提供了一个摄像机对象,它具有一个存储为2d向量的中心和范围。

// calculate the lowest and highest worldspace values of the cam
min = cam.center - cam.extent
max = cam.center + cam.extent

// subtract with tilemap corners and divide by tilesize to get 
// the anount of tiles that is outside of the cameras scoop
floor = Math.floor( min - ( tilemap.center - tilemap.extent ) / tilesize)
ceil = Math.ceil( max - ( tilemap.center + tilemap.extent ) / tilesize)
// floor & ceil is 2D vectors

现在,如果地板高于0或任何轴上的ceil低于0,则表示相机铲外的瓷砖数量相同。

// check if there is any tiles outside to the left or above of camera
if(floor.x > 0)
    sx+=floor.x// set start number of sx to amount of tiles outside of camera
if(floor.y > 0)
    y+=floor.y // set startnumber of y to amount of tiles outside of camera

 // test if there is any tiles outisde to the right or below the camera
 if(ceil.x < 0)
     mx+=ceil.x // then add the negative value to mx (max x)
 if(ceil.y < 0)
     my+=ceil.y // then add the negative value to my (max y)

tilemap的正常渲染将从0到轴的tile数量,这使用循环内的循环来计算两个轴。但是由于上面的代码,x和y将始终粘在相机边框内的空间中。

// will loop through only the visible tiles
for(; y<my; y++)
    // x need to be reset each y iteration, start value are stored in sx
    for(x=sx; x<mx; x++)
       // render tile x in tilelayer y

希望这有帮助!