1
\$\begingroup\$

So I am working on a game right now, using Monogame as my framework, and it has come time to render my world. My world is made up of a grid (think Terraria but top-down instead of from the side), and it has multiple layers of grids in a single world. Knowing how inefficient it is to call SpriteBatch.Draw() a lot of times, I tried to implement a system where the tile would only be drawn if it wasn't hidden by the layers above it. The problem is, I'm getting worse performance by checking if it's hidden than when I just let everything draw even if it's not visible. So my question is: how to I efficiently check if a tile is hidden to cut down on the draw() calls?

Here is my draw code for a single layer, drawing floors, and then the tiles (which act like walls):

public void Draw(GameTime gameTime)
    {
        int drawAmt = 0;

        int width = Tile.TILE_DIM;
        int startX = (int)_parent.XOffset;
        int startY = (int)_parent.YOffset;

        //Gets the starting tiles and the dimensions to draw tiles, so only onscreen tiles are drawn, allowing for the drawing of large worlds
        int tileDrawWidth = ((CIGame.Instance.Graphics.PreferredBackBufferWidth / width) + 4);
        int tileDrawHeight = ((CIGame.Instance.Graphics.PreferredBackBufferHeight / width) + 4);
        int tileStartX = (int)MathHelper.Clamp((-startX / width) - 2, 0, this.Width);
        int tileStartY = (int)MathHelper.Clamp((-startY / width) - 2, 0, this.Height);

        #region Draw Floors and Tiles
        CIGame.Instance.GraphicsDevice.SetRenderTarget(_worldTarget);
        CIGame.Instance.GraphicsDevice.Clear(Color.Black);
        CIGame.Instance.SpriteBatch.Begin();

        //Draw floors
        for (int x = tileStartX; x < (int)MathHelper.Clamp(tileStartX + tileDrawWidth, 0, this.Width); x++)
        {
            for (int y = tileStartY; y < (int)MathHelper.Clamp(tileStartY + tileDrawHeight, 0, this.Height); y++)
            {
                //Check if this tile is hidden by layer above it
                bool visible = true;
                for (int i = this.LayerNumber; i <= _parent.ActiveLayer; i++)
                {
                    if (this.LayerNumber != (_parent.Layers - 1) && (_parent.GetTileAt(x, y, i + 1).Opacity >= 1.0f || _parent.GetFloorAt(x, y, i + 1).Opacity >= 1.0f))
                    {
                        visible = false;
                        break;
                    }
                }

                //Only draw if visible under the tile above it
                if (visible && this.GetTileAt(x, y).Opacity < 1.0f)
                {
                    Texture2D tex = WorldTextureManager.GetFloorTexture((Floor)_floors[x, y]);
                    Rectangle source = WorldTextureManager.GetSourceForIndex(((Floor)_floors[x, y]).GetTextureIndexFromSurroundings(x, y, this), tex);
                    Rectangle draw = new Rectangle(startX + x * width, startY + y * width, width, width);

                    CIGame.Instance.SpriteBatch.Draw(tex, draw, source, Color.White * ((Floor)_floors[x, y]).Opacity);
                    drawAmt++;
                }
            }
        }

        //Draw tiles
        for (int x = tileStartX; x < (int)MathHelper.Clamp(tileStartX + tileDrawWidth, 0, this.Width); x++)
        {
            for (int y = tileStartY; y < (int)MathHelper.Clamp(tileStartY + tileDrawHeight, 0, this.Height); y++)
            {
                //Check if this tile is hidden by layers above it
                bool visible = true;
                for (int i = this.LayerNumber; i <= _parent.ActiveLayer; i++)
                {
                    if (this.LayerNumber != (_parent.Layers - 1) && (_parent.GetTileAt(x, y, i + 1).Opacity >= 1.0f || _parent.GetFloorAt(x, y, i + 1).Opacity >= 1.0f))
                    {
                        visible = false;
                        break;
                    }
                }

                if (visible)
                {
                    Texture2D tex = WorldTextureManager.GetTileTexture((Tile)_tiles[x, y]);
                    Rectangle source = WorldTextureManager.GetSourceForIndex(((Tile)_tiles[x, y]).GetTextureIndexFromSurroundings(x, y, this), tex);
                    Rectangle draw = new Rectangle(startX + x * width, startY + y * width, width, width);

                    CIGame.Instance.SpriteBatch.Draw(tex, draw, source, Color.White * ((Tile)_tiles[x, y]).Opacity);
                    drawAmt++;
                }
            }
        }

        CIGame.Instance.SpriteBatch.End();

        Console.WriteLine(drawAmt);
        CIGame.Instance.GraphicsDevice.SetRenderTarget(null); //TODO: Change to new rendertarget instead of null
        #endregion
    }

So I was wondering if this is an efficient way, but I'm going about it wrongly, or if there is a different, more efficient way to check if the tiles are hidden.

EDIT: For example of how much it affects performance: using a world with three layers, allowing everything to draw no matter what gives me 60FPS, but checking if its visible with all of the layers above it gives me only 20FPS, while checking only the layer immediately above it gives me a fluctuating FPS between 30 and 40FPS.

\$\endgroup\$
2

1 Answer 1

1
\$\begingroup\$

The first thing I would consider is exactly how many times you're going to call SpriteBatch.Draw() in each frame. You might find that it's not as inefficient as you think and sometimes the simplest implementation is good enough.

That said, there are lots of ways to optimize your code and rather than trying to spell it out here I suggest you read this great answer.

At a guess, without being able to see your project structure, I would say you might be hitting the batch limit because you're using too many textures. The solution is to use texture atlases (aka sprite sheets).

Okay, now to the real meat of your problem. You seem to be doing a lot of work inside your loops. It concerns me that you have a 3rd loop inside your first 2 loops to check the layer visibility and that you repeat nearly identical code after drawing floors to draw tiles.

It's also hard to tell if GetTileAt, GetFloorAt, GetTileTexture, GetSourceForIndex or GetTextureIndexFromSurroundings are efficient operations. If any of them are not, it's going to affect performance because your repeating them for every tile twice.

Some other things you can do is move any calculations outside the loops that don't need to be repeated more than once. For example:

int xMax =  (int)MathHelper.Clamp(tileStartX + tileDrawWidth, 0, this.Width)
for (int x = tileStartX; x < xMax; x++)

But honestly, I don't think your problem is going to be solved by a few simple tweaks. I'm guessing that most of the tiles don't change between frames so I would consider re-factoring the code to pre-calculate as much information as possible before getting into the draw calls.

You could take a look at some open source tile rendering engines like xTile because they are already reasonably efficient, or better yet you might be able to use tIDE in your project and get the benefit of map editor.

Last but not least, if you have access to a performance profiler, use it to help you find the problems. Doing it by eye is hard at best and even the experienced guys use profilers.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the link. I ended up using my own system of recursively checking if the tiles are hidden, then storing them in a 2D array of bools to check if they were drawn or not. This way lets me draw a 300x300 world with 5 layers in 4 milliseconds. But your link had a lot of great suggestions, so thanks for that. \$\endgroup\$
    – sm81095
    Commented Jun 28, 2013 at 19:19
  • \$\begingroup\$ Actually, I was thinking about your problem afterwards and I thought about storing the layers in arrays of bools too. Nice to know we came to the same conclusion and I'm glad it worked out for you. \$\endgroup\$ Commented Jun 29, 2013 at 4:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .