Xna 2D平铺引擎渲染问题

时间:2013-12-13 13:34:25

标签: c# xna

这是我的第一篇文章,如果我犯了任何错误,请道歉。 如果在帖子的最后,您需要更多信息,那么我会更乐意发布更多信息。

我一直在研究基于2D平铺的游戏,我最近尝试了一种不同的方法来渲染平铺。但是现在我在渲染瓷砖时遇到了性能问题。

游戏的世界分为16 * 256个块,每个块都有自己的RenderTarget2D(16 * 256)来保存图块数据。我已经将渲染目标称为块缓冲区。

    public void updateChunks()
    {
        int loadMinX = (int)( ( game.camera.location.X - game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
        int loadMaxX = (int)( ( game.camera.location.X + game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );

        int minX = (int)( game.player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) - (int)Math.Floor( chunkCount / 2f );
        int maxX = (int)( game.player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) + (int)Math.Floor( chunkCount / 2f );

        Chunk currentChunk;
        int cursorChunk = game.cursor.chunkLocation;

        for( int c = minX; c <= maxX; c++ )
        {
            if( chunkBuffer.ContainsKey( c ) )
            {
                currentChunk = chunkBuffer[c];

                if( c >= loadMinX && c <= loadMaxX )
                {
                    currentChunk.bufferBuilt = false;
                }

                if( currentChunk.highlighted == true )
                {
                    currentChunk.highlighted = false;
                    currentChunk.bufferBuilt = false;
                }

                if( chunkBuffer.ContainsKey( cursorChunk ) && chunkBuffer[cursorChunk] == currentChunk )
                {
                    currentChunk.highlighted = true;
                    currentChunk.bufferBuilt = false;
                }

                if( currentChunk.bufferBuilt == false )
                {
                    RebuildChunk( currentChunk );
                }
            }
        }

        game.tilesLoaded = chunkBuffer.Count * Chunk.chunkWidth * Chunk.chunkHeight;
    }

每个tick都会调用此方法,并决定需要重建/重绘哪个块缓冲区。目前,视口中的块会在每个刻度上重建,并且鼠标所在的块也会重建(因为块会更改为突出显示的颜色。)

将块存储在字典中,并通过循环通过minX访问加载的块 - &gt; chunk的字典的minY值,如updateChunks()方法所示。

    public void RebuildChunk( Chunk chunk )
    {
        BuildChunk( chunk );

        chunk.bufferBuilt = true;
    }

    private void BuildChunk( Chunk chunk )
    {
        int chunkWidth = Chunk.chunkWidth;
        int chunkHeight = Chunk.chunkHeight;
        int chunkLocation = chunk.id * chunkWidth;

        int loadMinX = (int)( ( game.camera.location.X - game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth ) - 2;
        int loadMaxX = (int)( ( game.camera.location.X + game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth ) + 2;
        int loadMinY = (int)( ( game.camera.location.Y - game.GraphicsDevice.Viewport.Height / 2 ) / Tile.tileHeight ) - 2;
        int loadMaxY = (int)( ( game.camera.location.Y + game.GraphicsDevice.Viewport.Height / 2 ) / Tile.tileHeight ) + 2;

        game.GraphicsDevice.SetRenderTarget( chunk.buffer );
        game.GraphicsDevice.Clear( Color.CornflowerBlue );

        game.spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied );

        for( int x = 0; x < chunkWidth; x++ )
        {
            for( int y = 0; y < chunkHeight; y++ )
            {
                if( x + chunkLocation > loadMinX && x + chunkLocation < loadMaxX && y > loadMinY && y < loadMaxY )
                {
                    if( chunk.tiles[x, y].type == 0 && chunk.tiles[x, y].lightSource == false )
                    {
                        chunk.tiles[x, y].lightSource = true;
                        chunk.tiles[x, y].lightComponent = new Light( game, new Point( chunkLocation + x, y ), 1.0f, 6 );
                    }

                    if( chunk.tiles[x, y].lightSource == true )
                    {
                        if( chunk.tiles[x, y].lightComponent == null )
                        {
                            chunk.tiles[x, y].lightSource = false;
                        }
                        else
                        {
                            ApplyLighting( chunk, x, y, chunk.tiles[x, y].lightComponent );
                        }
                    }

                    float brightness = chunk.tiles[x, y].brightness;

                    if( chunk.tiles[x, y].type == 0 )
                    {
                        if( chunk.highlighted == true && game.cursor.tileLocation.X == x && game.cursor.tileLocation.Y == y )
                        {
                            game.spriteBatch.Draw( game.gameContent.airTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( 0.5f, 0.5f, 0.5f ) );
                        }
                        else
                        {
                            game.spriteBatch.Draw( game.gameContent.airTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
                        }
                    }
                    else if( chunk.tiles[x, y].type == 1 )
                    {
                        if( chunk.highlighted == true && game.cursor.tileLocation.X == x && game.cursor.tileLocation.Y == y )
                        {
                            game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( 0.5f, 0.5f, 0.5f ) );
                        }
                        else
                        {
                            game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
                        }
                    }
                }
            }
        }

        game.spriteBatch.End();

        game.GraphicsDevice.SetRenderTarget( null );
    }

RebuildChunk负责再次构建块并重置构建的标志,这样就不会浪费cpu时间来重建尚未编辑或不在视口中的块。

BuildChunk是将磁贴绘制到块缓冲区的位置。渲染目标被交换到块缓冲区,然后循环遍历块块。

if( x + chunkLocation > loadMinX && x + chunkLocation < loadMaxX && y > loadMinY && y < loadMaxY )

此行检查图块是否在视口中。 如果是,则检查以查看切片类型是什么,然后为该切片绘制相应的纹理。那里也有照明逻辑,但它不会影响我的问题。

我的主要渲染逻辑是:

protected override void Draw( GameTime gameTime )
    {
        frameCounter++;

        drawScene( gameTime );

        GraphicsDevice.Clear( Color.White );

        spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied );

        spriteBatch.Draw( white, new Rectangle( 0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height ), Color.CornflowerBlue );
        spriteBatch.Draw( worldBuffer, new Rectangle( 0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height ), Color.White );

        spriteBatch.End();

        debugUI.Draw( gameTime );

        base.Draw( gameTime );
    }

    private void drawScene( GameTime gameTime )
    {
        int loadMinX = (int)( ( camera.location.X - GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
        int loadMaxX = (int)( ( camera.location.X + GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );

        int minX = (int)( player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) - (int)Math.Floor( worldManager.chunkCount / 2f );
        int maxX = (int)( player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) + (int)Math.Floor( worldManager.chunkCount / 2f );

        GraphicsDevice.SetRenderTarget( worldBuffer );
        GraphicsDevice.Clear( Color.CornflowerBlue );

        spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, camera.GetViewTransformation( GraphicsDevice ) );

        for( int c = minX; c <= maxX; c++ )
        {
            if( c >= loadMinX && c <= loadMaxX )
            {
                if( worldManager.chunkBuffer.ContainsKey( c ) )
                {
                    Chunk currentChunk = worldManager.chunkBuffer[c];

                    if( currentChunk.bufferBuilt == true )
                    {
                        spriteBatch.Draw( currentChunk.buffer, new Rectangle( c * Chunk.chunkWidth * Tile.tileWidth, 0, Chunk.chunkWidth * Tile.tileWidth, Chunk.chunkHeight * Tile.tileHeight ), Color.White );
                    }
                }
            }
        }

        spriteBatch.Draw( player.material, new Rectangle( (int)player.location.X, (int)player.location.Y, 16, 32 ), Color.White );

        spriteBatch.End();
        GraphicsDevice.SetRenderTarget( null );
    }

最后,在我的输入管理器中,我有以下内容:

if( mouseState.LeftButton == ButtonState.Pressed )
        {
            game.worldManager.RemoveTile( game.cursor.chunkLocation, game.cursor.tileLocation );
        }

调用:

public void RemoveTile( int chunkLocation, Point location )
    {
        if( chunkBuffer.ContainsKey( chunkLocation ) && location.X >= 0 && location.X < Chunk.chunkWidth && location.Y >= 0 && location.Y < Chunk.chunkHeight )
        {
            if( chunkBuffer[chunkLocation].tiles[location.X, location.Y].type != 0 )
                chunkBuffer[chunkLocation].tiles[location.X, location.Y].type = 0;
        }
    }

这只是我改变不同瓷砖状态的测试方法,在这种情况下,它会将污垢类型改为空气,以模拟去除瓷砖。

现在出现问题。当游戏第一次运行时,它很好,并保持在60fps。我将我的角色向右移动,用瓷砖填充屏幕宽度,然后我挖掘下来用瓷砖填充屏幕高度。

http://i.imgur.com/NBJ8qRj.png

http://i.imgur.com/N9i8asW.png (注意:第二张图片中的灯光已关闭)

在第一张图片中游戏仍然正常运行,但是当我开始在第二张图片中移除玩家周围的牌时,一段时间后帧率从60降到10以下并降到2-3 fps左右即使我已停止点击/对游戏做任何事情,也会永远保持这个速度并且无法恢复。

问题出现在全屏分辨率下,性能分析表明在BuildChunk()方法内的绘制调用期间会使用大量的cpu时间:

else
                    {
                        game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
                    }

那么它可能是由于不断交换纹理而引起的吗?即;从绘制airTexture交换到dirtTexture等?因为我发现如果我将airTexture改为dirtTexture,问题就会消失。

如果我在我移除的所有瓷砖下面进一步挖掘,fps会恢复到60,所以我的猜测是它与在重建阶段绘制纹理有关。

如果有人可以查看这些代码,并指出任何有缺陷的内容,我们将不胜感激,并且可能的解决方案会很棒。

此外,如果某人有使用瓷砖的这种情况的经验,并提出您的建议将是非常宝贵的!

1 个答案:

答案 0 :(得分:3)

我的天哪,这是一个彻底的问题!向我们提供大量信息是值得赞赏的,但将来尝试将您的问题简化为与直接相关的事物可能是一个好主意。否则人们会不堪重负。

我可以告诉你的是,我认为你的分析正处于正确的轨道上。正如您所猜测的,来回交换纹理可以对性能产生重大影响。

对2D渲染性能的最大限制之一是所谓的批量限制。基本上,每次你告诉图形设备绘制一些东西 - 也就是说,每次你调用一个DirectX的DrawFooPrimitives函数 - 都会产生一定的固定开销。因此,制作大量的绘图调用,每个绘制调用只绘制少量多边形,效率非常低。你的GPU处于空闲状态,而你的CPU则会处理绘制调用!

XNA的SpriteBatch类存在以解决此问题。它将大量精灵组装到单个缓冲区中,只需调用DrawIndexedPrimitives即可绘制。但是,如果它们共享相同的图形设备状态,它只能批量精灵。这包括您在SpriteBatch.Begin()中设置的采样器和光栅化器状态,​​但它还包括精灵纹理。更改纹理会强制SpriteBatch刷新当前批次并启动一个全新的批处理。

那就是说,你正在犯另一个相关的错误,这种错误使得上述观点无关紧要。在您的渲染方法中,您使用SpriteBatch.Begin()调用SpriteSortMode.Immediate。对于大量的精灵而言,这将是非常低效的,因为你所做的就是说,“不要批量处理这些精灵;为我绘制的每一个精灵调用{em} {<1}} “。精灵是4个顶点。您的显卡每次DrawIndexedPrimitives通话可能会处理数十万次。一个巨大的浪费!

总而言之,这是我在代码中会改变的内容:

  1. Draw更改为SpriteSortMode.Immediate。使用此选项,SpriteSortMode.Deferred将累积您绘制到缓冲区中的精灵,然后在您调用SpriteBatch时立即绘制所有精灵。
  2. 为所有瓷砖使用单个纹理。这通常使用所谓的texture atlas来完成。您可以在End()中指定一个源矩形,它将告诉它仅使用较大图像的一小部分。