【问题标题】:How to draw thousands of tiles without killing FPS如何在不杀死FPS的情况下绘制数千个瓷砖
【发布时间】:2026-02-23 05:50:01
【问题描述】:

我到处寻找解决此问题的方法(我可能只是盲目地看到周围的解决方案)。我的游戏目前在屏幕上渲染瓷砖地图,并且不会渲染实际上不在屏幕范围内的瓷砖。但是,每个图块都是 16x16 像素,这意味着如果屏幕上的每个像素都包含一个分辨率为 1920x1080 的图块,则要绘制 8100 个图块。

每个周期都画这么多图块真的会杀死我的 FPS。如果我运行 800x600 分辨率,我的 FPS 会达到 ~20,而在 1920x1080 时,它的运行速度约为 3-5 FPS。这真让我抓狂。

我尝试过线程化和使用异步任务,但那些只是在屏幕上闪烁。可能只是我编码错误。

这是我目前使用的绘图代码。

        // Get top-left tile-X
        Vector topLeft = new Vector(Screen.Camera.X / 16 - 1, 
            Screen.Camera.Y / 16 - 1);
        Vector bottomRight = new Vector(topLeft.X + (Screen.Width / 16) + 2, 
            topLeft.Y + (Screen.Height / 16) + 2);

        // Iterate sections
        foreach (WorldSection section in Sections)
        {
            // Continue if out of bounds
            if (section.X + ((Screen.Width / 16) + 2) < (int)topLeft.X ||
                section.X >= bottomRight.X)
                continue;

            // Draw all tiles within the screen range
            for (int x = topLeft.X; x < bottomRight.X; x++)
                for (int y = topLeft.Y; y < bottomRight.Y; y++)
                    if (section.Blocks[x - section.X, y] != '0')
                        DrawBlock(section.Blocks[x - section.X, y], 
                            x + section.X, y);
        }

有 8 到 12 个部分。每个图块由二维数组中的一个 char 对象表示。

画块方法:

public void DrawBlock(char block, int x int y)
    {
        // Get the source rectangle
        Rectangle source = new Rectangle(Index(block) % Columns * FrameWidth,
            Index(block) / Columns * FrameHeight, FrameWidth, FrameHeight);

        // Get position
        Vector2 position = new Vector2(x, y);

        // Draw the block
        Game.spriteBatch.Draw(Frameset, position * new Vector2(FrameWidth, FrameHeight) - Screen.Camera, source, Color.White);
    }

Index()方法只是返回char对应的tile的frame index。

我想知道如何才能真正允许一次绘制这么多而不以这种方式降低帧速率。我提供的代码显然不是很优化,还是我应该做一些特定的事情才能在不降低性能的情况下绘制这么多单独的图块?

【问题讨论】:

  • 您附加的代码似乎没问题,我会在 DrawBlock() 或 Sections 中寻找性能问题。请发布您的代码的这些部分。
  • 每个部分一个draw call怎么样?如果一个部分是一个小块网格,说 (3x3) 而不是 9 个调用,则将它们绘制一次到合适的图像中,然后只绘制它。您甚至可以即时预构建这些部分,缓存它们并绘制它们。如果缓存变大,请丢弃旧部分并重新构建新部分。
  • 在这里您可以找到解决方案:Link“对于我的 2D RPG,这造成了 51 FPS 和 1,300 FPS 之间的差异。”
  • 非常次要的 section.Blocks[x - section.X, y] 调用了两次并且(假设没有副作用)您最好将其存储在变量中并重用。 Defo 发布 DrawBlock,以及您如何以及在哪里测量您的 FPS?
  • 诀窍是尽可能少地绘制,或者至少尽可能少地调用。对于面向块/图块的游戏,这通常意味着将图块分成一个更大的组,然后将其缓存,以便您可以绘制每个块一次。如果你的块永远不会改变,这是非常有效的,并且可以在关卡加载时完成。如果他们这样做了,您仍然只需要在实际更改时重新创建一个块,让您可以摊销平局。

标签: c# xna tiles-game


【解决方案1】:

不确定这是否是解决问题的最佳方法,但我已经开始使用 RenderTarget2D 将世界的大块预渲染为纹理。我必须一次在实际屏幕边界周围的给定区域内加载块,因为一次加载所有块会使其内存不足。

当您接近当前预渲染区域的边界时,它将根据您在世界中的新位置重新处理块。处理过程大约需要 100 毫秒,因此在加载新区域时,玩家会在这段时间内感到轻微的减速。我不是很喜欢那样,但至少现在 FPS 是 60。

这是我的块处理器:

    public bool ProcessChunk(int x, int y)
    {
        // Create render target
        using (RenderTarget2D target = new RenderTarget2D(Game.CurrentDevice, 16 * 48, 16 * 48,
            false, SurfaceFormat.Color, DepthFormat.Depth24))
        {

            // Set render target
            Game.CurrentDevice.SetRenderTarget(target);

            // Clear back buffer
            Game.CurrentDevice.Clear(Color.Black * 0f);

            // Begin drawing
            Game.spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend);

            // Get block coordinates
            int bx = x * 48,
                by = y * 48;

            // Draw blocks
            int count = 0;
            foreach (WorldSection section in Sections)
            {
                // Continue if section is out of chunk bounds
                if (section.X >= bx + 48) continue;

                // Draw all tiles within the screen range
                for (int ax = 0; ax < 48; ax++)
                    for (int ay = 0; ay < 48; ay++)
                    {
                        // Get the block character
                        char b = section.Blocks[ax + bx - section.X, ay + by];

                        // Draw the block unless it's an empty block
                        if (b != '0')
                        {
                            Processor.Blocks[b.ToString()].DrawBlock(new Vector2(ax, ay), true);
                            count++;
                        }
                    }
            }

            // End drawing
            Game.spriteBatch.End();

            // Clear target
            target.GraphicsDevice.SetRenderTarget(null);

            // Set texture
            if (count > 0)
            {
                // Create texture
                Chunks[x, y] = new Texture2D(Game.CurrentDevice, target.Width, target.Height, true, target.Format);

                // Set data
                Color[] data = new Color[target.Width * target.Height];
                target.GetData<Color>(data);
                Chunks[x, y].SetData<Color>(data);

                // Return true
                return true;
            }
        }

        // Return false
        return false;
    }

如果有任何关于如何改进这种方法的建议,我不会难过!

感谢您在这里提供的帮助!

【讨论】:

  • 重用 RenderTarget2D 对象而不是每次都创建一个新对象怎么样?这会缩短 100 毫秒吗?
  • 还可以尝试在注释掉的 foreach(Section 中的 WorldSection 部分)的情况下运行它。看看这 100 毫秒中有多少是由渲染目标切换引起的,有多少是由实际绘制引起的。
  • 仅使用一个 RenderTarget2D 对象将处理时间减少了几毫秒,但几乎没有引起注意。摆脱世界部分循环没有明显的效果,可能是因为每次它处理大约 20 个左右的块,其中有 8-12 个部分需要循环。除了实际渲染之外,没有太多工作。