【问题标题】:Xna 2D Tile Engine Rendering IssueXna 2D 平铺引擎渲染问题
【发布时间】:2013-12-13 13:34:25
【问题描述】:

这是我的第一篇文章,如果我犯了任何错误等,敬请见谅。 如果在帖子末尾,您需要更多信息,那么我会很乐意发布更多信息。

我从事基于 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 -> 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 ) );
                    }

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

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

如果有人可以查看此代码并指出其中的任何缺陷,将不胜感激,并且可能的解决方案会很棒。

另外,如果有人在这种情况下使用瓷砖并提供您的建议将是非常宝贵的!

【问题讨论】:

  • 下次尽量只贴相关代码,这个问题很大,不知道有多少人会看。

标签: c# xna


【解决方案1】:

天哪,这是一个彻底的问题!感谢您向我们提供大量信息,但在未来,尝试将您的问题减少到直接相关的内容可能是个好主意。否则人们会不知所措。

我可以告诉你的是,我认为你的分析是正确的。如您所料,来回交换纹理会对性能产生重大影响。

2D 渲染性能的最大限制之一是所谓的batch limit。基本上,每次你告诉图形设备绘制一些东西时——也就是说,每次你调用 DirectX 的DrawFooPrimitives 函数之一时——都会有一定的固定开销。因此,进行大量的绘制调用,每个只绘制少量的多边形是非常低效的。当您的 CPU 忙于处理绘图调用时,您的 GPU 处于空闲状态!

XNA 的SpriteBatch 类可以解决这个问题。它将许多精灵组装到一个缓冲区中,只需调用DrawIndexedPrimitives 即可绘制该缓冲区。但是,只有当精灵共享相同的图形设备状态时,它才能批处理精灵。这包括您在SpriteBatch.Begin() 中设置的采样器和光栅化器状态等内容,但它还包括精灵纹理。更改纹理会强制 SpriteBatch 刷新当前批次并开始一个全新的批次。

也就是说,您犯了另一个相关的错误,使上述观点变得无关紧要。在您的渲染方法中,您使用SpriteSortMode.Immediate 调用SpriteBatch.Begin()。这对于大量精灵来说效率非常低,因为你所做的是说,“根本不要批量处理这些;为我绘制的每一个精灵调用DrawIndexedPrimitives。”一个精灵是 4 个顶点。每个Draw 调用您的显卡可能可以处理数十万个。巨大的浪费!

所以,总而言之,这是我将在您的代码中更改的内容:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-23
    • 1970-01-01
    • 2016-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-30
    相关资源
    最近更新 更多