2

I've looked everywhere for a workaround to this issue (I may just be blind to see the solutions lying around). My game currently renders the tilemap on the screen and will not render tiles that are not actually within the screen bounds. However, each tile is 16x16 pixels, that means 8100 tiles to draw if every pixel on the screen contains a tile at 1920x1080 resolution.

Drawing that many tiles every cycle really kills my FPS. If I run 800x600 resolution my FPS goes to ~20, and at 1920x1080 it runs at around 3-5 FPS. This really drives me nuts.

I've tried threading and using async tasks, but those just flicker the screen. Probably just me coding it incorrectly.

Here's the drawing code that I currently use.

        // 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);
        }

There are between 8 and 12 sections. Each tile is represented by a char object in the two-dimensional array.

Draw block method:

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);
    }

The Index() method just returns the frame index of the tile corresponding to the char.

I'm wondering how I could make it possible to actually allow this much to be drawn at once without killing the framerate in this manner. Is the code I provided clearly not very optimized, or is it something specific I should be doing to make it possible to draw this many individual tiles without reducing performance?

Sarkilas
  • 76
  • 7
  • The code you attached seems ok, I'd look for performance issues in DrawBlock() or Sections. Please post these parts of your code. – borkovski Oct 10 '13 at 13:56
  • How about one draw call for each section? If a section is a grid of tileslets say (3x3) instead of 9 calls, draw them once into a fitting image and only draw that. You could even prebuilt these sections on the fly, cache them and draw them. If the cache grows to big, throw away old sections and built the new ones again. – dowhilefor Oct 10 '13 at 14:03
  • very minor by section.Blocks[x - section.X, y] called twice and (assuming no side effects) you may be better storing that in a variable and reusing. Defo post DrawBlock, also how and where are you measuring your FPS? – tolanj Oct 10 '13 at 14:09
  • Added DrawBlock() reference. Looking into the answer made by Daniloloko for now. – Sarkilas Oct 10 '13 at 14:28
  • 1
    The trick is to draw as little as possible, or at least make as few calls as you can. In the case of block/tile oriented games, that usually means chunking the tiles into a larger group, then cached that so you can draw each chunk once. If your chunks never change, this is incredibly efficient and can be done when the level loads. If they do, you still only have to recreate a chunk when it actually has been changed, allowing you to amortize the draws way down. – ssube Oct 10 '13 at 14:30
  • This isn't the issue, but could help improve the performance by drawing less. If you have lighting in your game, don't draw the dark tiles. If you have objects covering tiles, or background tiles behind other tiles, don't draw them. – Cyral Oct 10 '13 at 22:15
  • But what would I have done if I didn't have dark tiles? The thing is, the blocks are changable by users, and if they do run a high resolution and decide to fill the screen with blocks, they will still be lit, and causing them to draw. So I need to be able to handle that without such insane performance issues. I've tried rendering chunks to textures to make less drawing calls, but this just kills memory instead. So it's like, there's always something that doesn't want to work with me. It's driving me nuts. – Sarkilas Oct 11 '13 at 00:21
  • Here you will find the solution: [Link](http://www.gamedev.net/topic/641734-drawing-large-tile-maps/) "For my 2D RPG this made the difference between 51 FPS and 1,300 FPS." – Danilo Breda Oct 10 '13 at 14:06
  • I must admit I am sure this is the solution to my problem, but I can't make what is told into actual code in my head. I imagine using Render Targets, but I don't know if that's what I am looking for. – Sarkilas Oct 10 '13 at 14:40
  • I'm unable to see how I can buffer the draw call. I don't seem to be able to find a workaround. The texture used for drawing all the blocks is the exact same texture tileset, so the GPU does not need to change the texture during any of the calls. Yet, reducing the amount of draw calls from (Screen Width / Block Width) * (Screen height / Block Height) to something lower is something I can't quite fathom. How do I split the draw calls when I need all the separate blocks to be drawn anyway? Is there another way to draw them? I'm quite lost. – Sarkilas Oct 10 '13 at 16:42

1 Answers1

0

Not sure if this is the best way to deal with the problem, but I've started to use RenderTarget2D to pre-render chunks of the world into textures. I have to load chunks within a given area around the actual screen bounds at a time, because loading all chunks at once will make it run out of memory.

When you get close to the bounds of the current pre-rendered area, it will re-process chunks based on your new position in the world. The processing takes roughly 100 milliseconds, so when loading new areas the player will feel a slight slowdown for this duration. I don't really like that, but at least the FPS is 60 now.

Here's my chunk processor:

    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;
    }

If there are any suggestions on how this approach can be improved, I won't be sad to hear them!

Thanks for the help given here!

Sarkilas
  • 76
  • 7
  • What about reusing RenderTarget2D objects instead of creating a new one each time? does that bring the 100ms down? – Weyland Yutani Oct 11 '13 at 12:34
  • Also try running it with the foreach (WorldSection section in Sections) commented out. To see how much of that 100ms is caused by the rendertarget switching and how much is caused by the actual drawing. – Weyland Yutani Oct 11 '13 at 12:36
  • Using only one RenderTarget2D object reduced the process time by a few milliseconds, but barely noticable. Getting rid of the world section loop had no noticable effect, possibly because each time it processes around 20 or so chunks, where there's between 8-12 sections to loop through. Not a whole lot of work except for the actual rendering. – Sarkilas Oct 11 '13 at 13:13