Drawing large tile maps

Started by
4 comments, last by GameDev.net 11 years ago

Hello everyone!

I've been recently working on 2d isometric tile engine using XNA as school project. I've set up the engine to limit drawing only to what is inside the camera view. Problem is as soon as I start zooming out and therefore increasing the amount of tiles to be drawn the framerate starts to drop really fast. Around 100x100x3 (3 is the amount of layers for grass, roads etc) draw calls is enough to go from 60 to 50 FPS.

So how do other tile based games handle drawing large areas? For example I remember in OpenTTD you could zoom pretty far out and have huge maps (something like 2048x2048) yet it was really smooth. Thank you in advance for any solution/different approach you might have.

Also I'm new here, so hi again! I hope this is the right section for this topic.

Advertisement

You compile your tiles into larger bits. Take each 512x512, or whatever, pixel section of tiles and compile them all into their own texture. This will DRASTICALLY cut down on the amount of draw calls you have to make. It will make loading longer though, but not that much really.

If your tiles are 16 x 16 pixels, this means for each 512 x 512 pixels you cut down your calls down 1023 for that section.

If you are using XNA, the sprite batching class should optimize 2D drawing automatically. If you are using it, do you have all your tiles on the same texture or on a different texture? If you have them on different texture, it's impossible to batch the draw calls because the video card is constantly swapping textures.

The problem is that draw calls have some overhead that happens whether you are drawing 1 triangle or 10,000. The video card works asynchroneously so this is not a problem for the first draw call, but if the next one happens while the video card is still busy, the CPU has to wait until the video card is idle and this is the problem. This is why issuing 30,000 (100 x 100 x 3) draw calls in quick succession every frame is going to seriously drain your performance.

The trick to drawing multiple tiles without killing your framerate is to batch your draw requests into a single draw call. Any tiles using the same tileset and the same render states can be batched together. So basically, put all your tiles into one tileset, and if you need tiles with different blend states (additive and subtractive blending is popular in 2D game) put them on their own layer so they're all grouped together.

Basically, when you call your Draw() method, don't issue a draw call immediately. Instead, put the call into a buffer and only do the draw call when you have to. This is usually when you have to draw something with another texture or render state or when you're about to flip buffer, but you can also have a Flush() method to force it if you want to optimize. It adds some logistic because you have to make sure that no texture is deleted while it is still waiting to be drawn, but this was not a big deal for me.

For my 2D RPG this made the difference between 51 FPS and 1,300 FPS. smile.png

Welcome to GameDev.net!

What I'd do is break your 100x100 map into 10x10 or 20x20 chunks. One 'chunk' should be about the size of the width of the largest screen resolution you support, roughly. Then load and draw just the chunks around the player (9 chunks (3x3), 60x60 tiles if 20x20 tiles per chunk). Your maps can then be of any physical size you want, just streaming in, and unstreaming, the map chunks as you need them.

For example I remember in OpenTTD you could zoom pretty far out and have huge maps (something like 2048x2048) yet it was really smooth.

If you need zooming out, then when more than 3x3 chunks are visible on your screen, dynamically pre-render each chunk on a smaller image and draw that each frame. 50% zoom? Your chunk is (50% * ChunkSizeInTiles * TileSize).

Hey there MrZeo!

I'm curious how you are drawing your tiles. Are you doing something like this:

for each tile

{

spritebatch.begin();

spritebatch.draw(tile)

spritebatch.end()

}

If so, that is likely the reason of your performance issues. You will want to do something like this:

spritebatch.begin()

for each tile

{

spritebatch.draw()

}

spritebatch.end()

The first method interrupts the graphics card with draw requests equal to the number of tiles you wish to render (10,000 tiles equals 10,000 draw calls), while the second method will interrupt the graphics card once (10,000 tiles equals 1 draw call.)

Thx for all the replies! I have to be able to interact with the map runtime so I don't think splitting it to textures would work. I was able to make some progress though. I dropped the whole idea of separate tile class and instead have the data in simple byte[x,y,z] array. I'm not really sure what was causing the slowdown before but now I can run steady 60 fps as long as I don't zoom too far out.

I probably have to change to SpriteSortMode.BackToFront so that I can utilize layerdepth for sorting. Even though I'm already naturally sorting the map itself, I have to somehow get depth sorted with moving objects that might be behind parts of the map.

Here is the draw/culling, not pretty but does the job:


        public void Draw(SpriteBatch spriteBatch)
        {
            // Start tile of horizontal row to be drawn.
            int dx = topLeft.X;
            int dy = topLeft.Y;

            // Tile to be drawn.
            int x, y;

            bool swap = true;

            spriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null, camera.GetViewMatrix());

            do
            {
                // Update tile to start coordinates of new row.
                x = dx;
                y = dy;

                do
                {
                    // Make sure we are within map limits.
                    if (x >= 0 && x < Map.X && y >= 0 && y < Map.Y)
                    {
                        for (int z = 0; z < Map.Z; z++)
                        {
                            if (data[x, y, z] > 0)
                            {
                                tilePosition = IsoTransform(x, y);
                                GetTileData(data[x, y, z], z, ref sourceRect, ref tileAlpha);
                                spriteBatch.Draw(tileSheet, tilePosition, sourceRect, tileColor * tileAlpha, 0, tileOrigin, 1f, SpriteEffects.None, 0);
                            }
                        }
                    }

                    // Add to draw our next horizontal tile.
                    x++; y--;

                } while (x <= dx + maxX); // Horizontal row done.

                // Add to either dx or dy to get to the next row.
                if (swap)
                {
                    dx += 1;
                    swap = false;
                }
                else
                {
                    dy += 1;
                    swap = true;
                }
            } while (dy <= topLeft.Y + maxY); // We are done drawing once we reach maxY.

            spriteBatch.End();
        }

        private void UpdateLimits()
        {
            // Get view rect coordinates.
            Vector2 topLeft = Vector2.Transform(new Vector2(0, 0), camera.InverseViewMatrix);
            Vector2 bottomRight = Vector2.Transform(screenSize, camera.InverseViewMatrix);

            // Calculate how many tiles fit in to this view.
            maxX = (int)(bottomRight.X - topLeft.X) / Tile.X;
            maxY = (int)(bottomRight.Y - topLeft.Y) / Tile.Y;

            // Get first tile to be drawn.
            this.topLeft = WorldToTile(topLeft, false);

            // Add a bit extra around the view to make sure half tiles get drawn.
            maxX += 3;
            maxY += 3;
            this.topLeft.X -= 2;
        }

This topic is closed to new replies.

Advertisement