繁体   English   中英

Xna 2D平铺引擎渲染问题

[英]Xna 2D Tile Engine Rendering Issue

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

我已经在基于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是将tile绘制到块缓冲区的位置。 渲染目标交换到块缓冲区,然后循环遍历块块。

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,所以我的猜测是它与在重建阶段绘制纹理有关。

如果有人可以查看这些代码并且可能指出任何缺陷,那将是值得赞赏的,并且可能的解决方案会很棒。

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM