Optimizing Draw calls in MonoGame Android platformer

| | August 10, 2015

I am working on a 2D platformer that is in the final stages of optimization. I have a few different devices for testing, and I am trying to get the best possible framerate on the weaker ones.

  • On a Nexus 5 I get a constant 60fps, with an occasional hiccup.

  • On a Galaxy S4 I also get a good 55-60fps

  • On an original Galaxy S (Vibrant) The frames drop to about 20-25

My question is, given the tile system described below, what can I do to increase the FPS on low-end devices?
And granted the Galaxy S came out years ago, should I even worry about it?

My background consists of 3 scrolling parallax layers.

The world I am drawing in a viewport of 25×15 tiles, at a size of 32x32px per tile, with 3 layers each (32x32x3px). The camera already culls tiles that are out of view. All tiles are found in one texture, and this is done in a single SpriteBatch call that locates the correct texture location for the tile layer. Code is below:

public void Draw(SpriteBatch spriteBatch, bool upsideDown)
{
    int startX = GetCellAtPixelX( (int)Camera.Position.X );
    int startY = GetCellAtPixelY( (int)Camera.Position.Y );

    int endX = GetCellAtPixelX( (int)Camera.Position.X + 
        Camera.ViewportWidth );
    int endY = GetCellAtPixelY( (int)Camera.Position.Y +
        Camera.ViewportHeight );

    for (int x = startX; x <= endX; x++)
    {
        for (int y = startY; y <= endY; y++)
        {
            for (int z = 0; z < MapLayers; z++)
            {
                if ((x >= 0) && (y >= 0) &&
                    (x < MapWidth) && (y < MapHeight))
                {
                    // ...

                    /// The very last parameter in this draw call is
                    /// the draw depth. This draw format will arrange
                    /// layers from front to back (0 = front, 1 = back)
                    /// so we subtract 1 from the z-index as a decimal
                    /// to get the appropriate location:
                    ///     Background      -> 0.7
                    ///     Middleground    -> 0.6
                    ///     Foreground      -> 0.5
                    spriteBatch.Draw(
                        tileSheet,
                        CellScreenRectangle(x, y),
                        TileSourceRectangle(mapCells[x,y].
                            GetLayerIndex((TileLayers)z)),
                        Color.White,
                        0.0f,
                        Vector2.Zero,
                        effect,
                        0.7f - ((float)z * 0.1f)
                    );
                }
            }
        } /// End y loop
    } /// End x loop
}

As you can see, it’s not very complex code. I have checked if my Update calls are slowing the game down, but it’s very clearly in the Draw function. When I leave out my call to world.Draw(), fps shoot up to about 50 on the Galaxy S. Also, if I draw only a single layer, I get better performance around 42-45 fps.

I can discard one layer if necessary, but I need at least 2 tile layers. How else can I speed up the draw calls?

I am using MonoGame for Android (XNA)

One Response to “Optimizing Draw calls in MonoGame Android platformer”

  1. craftworkgames on November 30, -0001 @ 12:00 AM

    What I would try to do is get rid of the draw depth calculation by drawing your tiles in the correct order to start with.

    What this means is, you’ll be drawing your back layer first, then the middle layer, then the top layer. If your tile sprites overlap (e.g. isometric view) you may also need to draw the back row first, then the next and so on and depending on the angle, maybe reverse the loops. I’d have to see a screenshot looks like to get a better idea.

    You should also do as many calculations as possible outside the loops. For example, rather than checking if X and Y are inside the bounds of the map in the inner most loop, you can do it before the loops even begin.

    Lastly, it concerns me that some of the methods inside the loop may be taking longer than an index lookup. For example, you need X, Y and Z to get the layer index so you can get the tile source rectangle. These seems excessive, but without knowing more about your implementation I can’t provide any suggestions.

    Here’s a partial re-factoring based on the above suggestions.

        public void Draw(SpriteBatch spriteBatch, bool upsideDown)
        {
            int startX = GetCellAtPixelX((int)Camera.Position.X);
            int startY = GetCellAtPixelY((int)Camera.Position.Y);
    
            int endX = GetCellAtPixelX((int)Camera.Position.X + Camera.ViewportWidth);
            int endY = GetCellAtPixelY((int)Camera.Position.Y + Camera.ViewportHeight);
    
            if (startX < 0) startX = 0;
            if (endX > MapWidth - 1) endX = MapWidth - 1;
            if (startY < 0) startY = 0;
            if (endY > MapHeight - 1) endY = MapHeight - 1;
    
            for (int z = 0; z < MapLayers; z++)
            {
                for (int y = startY; y <= endY; y++)
                {
                    for (int x = startX; x <= endX; x++)
                    {
                        var cellScreenRectangle = CellScreenRectangle(x, y);
                        var mapCell = mapCells[x, y];
                        var layerIndex = mapCell.GetLayerIndex((TileLayers) z);
                        var tileSourceRectangle = TileSourceRectangle(layerIndex);
    
                        spriteBatch.Draw(
                            tileSheet,
                            cellScreenRectangle,
                            tileSourceRectangle,
                            Color.White,
                            0.0f,
                            Vector2.Zero,
                            effect,
                            0);
                    } // End y loop
                } // End x loop
            } // End z loop
        }
    

    If you can provide a screenshot and some more information about the CellScreenRectangle, GetLayerIndex and TileSourceRectangle methods I’m sure there are more things that can be improved.

Leave a Reply