Need advice on optimizing python 2d tileset rendering

92 views Asked by At

I've been working on a 2D isometric tile-based MORPG for a few months now and realized that my game screen rendering is at a really low frame rate. I've been researching and testing for a few weeks now and can only make marginal gains to my frame rate. I've used cProfile and tested my frame rate and I can achieve 100+ FPS on the program normally, but once my "render()" function is called it drops to 5 FPS. Here is a (somewhat) condensed version of that function:

for y in range(0, 42):
        for x in range(0, 42):
            if (player.mapY + y - 21 > 0) and (player.mapY + y - 21 < 128) and (player.mapX + x - 21 > 0) and (
                                player.mapX + x - 21 < 128):
                if (startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX) > -64 and (startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX)+halfGraphicSizeX < 1024+32 and\
                    (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY) > -32 and (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY)+halfGraphicSizeY < 600+32:
                    if self.getGroundAtYX(player.mapY + (y - 21), player.mapX + (x - 21)) is not 0:

                        canvas.create_image((startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX),
                                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY),
                                            image=groundGraphics[
                                                self.getGroundAtYX(player.mapY + (y - 21), player.mapX + (x - 21))],
                                            anchor=NW)

                    if (self.getObjectAtYX(player.mapY + (y - 21), player.mapX + (x - 21)) is not 0):
                        canvas.create_image((startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX),
                                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 34),
                                            # -34 for img height diff between ground & objects
                                            image=objectGraphics[
                                                self.getObjectAtYX(player.mapY + (y - 21), player.mapX + (x - 21))],
                                            anchor=NW)


            ghostCopy = list(gameState.itemsOnGround)
            for i in range(0, len(ghostCopy)):
                if ghostCopy[i].idNum > 0:
                    if (player.mapX - 21 + x == ghostCopy[i].mapX and player.mapY - 21 + y ==
                        ghostCopy[i].mapY):
                        canvas.create_image((startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX),
                                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY),
                                            image=itemGraphics[ghostCopy[i].idNum],
                                            anchor=NW)

            ghostCopy = ""
            ghostCopy = list(gameState.monster)
            for i in range(0, len(ghostCopy)):
                if ghostCopy[i].active == True and ghostCopy[i].hp > 0:
                    if (player.mapX - 21 + x == ghostCopy[i].mapX and player.mapY - 21 + y ==
                        ghostCopy[i].mapY):
                        canvas.create_image((startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX),
                                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 34),
                                            # -34 for img height diff between ground & objects
                                            image=monsterGraphics[ghostCopy[i].type],
                                            anchor=NW)
                        canvas.create_rectangle(
                            (startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX) + 15,
                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 35),
                            (startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX) + 16 + 33,
                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 29), fill="black",
                            width=0)
                        canvas.create_rectangle(
                            (startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX) + 16,
                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 30),
                            (startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX) + 16 + (
                                32 * (ghostCopy[i].hp / ghostCopy[i].maxHp)),
                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 34), fill="green",
                            width=0)

            ghostCopy = ""
            ghostCopy = list(gameState.sprite)
            for i in range(0, len(ghostCopy)):
                if ghostCopy[i].graphic[0:1] == "0":

                if ghostCopy[i].active == True and ghostCopy[i].username != "ME":
                    if (player.mapX - 21 + x == ghostCopy[i].mapX and player.mapY - 21 + y ==
                        ghostCopy[i].mapY):
                        #"graphicToDraw" variable is derived from an animation state but has
                        #been removed from here to make it easier to read
                        canvas.create_image((startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX),
                                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 34),
                                            # -34 for img height diff between ground & objects
                                            image=graphicToDraw,
                                            anchor=NW)

            if (y == 21):
                if (x == 21):
                    #"graphicToDraw" variable is derived from an animation state but has
                    #been removed from here to make it easier to read
                    canvas.create_image(
                        (startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX),
                        (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 34),
                        # -34 for img height diff between ground & sprites
                        image=graphicToDraw,
                        anchor=NW)

            ghostCopy = ""
            ghostCopy = list(gameState.spells)
            for i in range(0, len(ghostCopy)):
                if ghostCopy[i].active:
                    if (player.mapX - 21 + x == ghostCopy[i].mapX and player.mapY - 21 + y ==
                        ghostCopy[i].mapY):
                        canvas.create_image((startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX),
                                            (startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY - 34),
                                            image=spellGraphics[ghostCopy[i].id],
                                            anchor=NW)

the render() function belongs to the map object (self refers to the map in this code segment). It effectively goes through the tiles -21 .. 21 on the x,y axis and if the tile is within the maps tile bounds (0 .. 128) and the tile is within the screen size (1024x600) it draws it to the screen.

The "ghostCopy" takes a snapshot of the current gamestate element (spells for example) so that it isn't updated by the thread receiving server data mid-iteration.

In some optimization testing I did I reduced the y, x range at the start to minimize the amount of total loop iterations. I read that using a texture atlas/spritesheet could improve rendering speed but I couldn't make an improvement using one.

I tried just manually drawing the amount of images that would normally render in a general scene in a for loop and got around 30+ fps. So my render function is 25 fps slower than it could be.

I'm assuming that the constant checks every loop iteration to whether the tile is within the screen could be optimized, but i'm not really sure how to do that without using a loop like this.

If anyone has any recommendations I would greatly appreciate it.. i've been stuck on this problem for weeks and haven't made any real progress with my game at all :(

** [EDIT] ** Most recommendations seem to be towards limiting the amount of mathematical expressions. I haven't had a chance to test this, but is it likely just limiting the amount of math would optimize frame rate considerably?

2

There are 2 answers

1
Scott Hunter On

You could pull all expressions involving constants and y to be computed just inside the outer (for y) loop; for example, player.mapY + y - 21, y * halfGraphicSizeX, y * halfGraphicSizeY, etc.: computer each just once, tuck in a variable, and use throughout the code. Similarly for x, but not quite as effective.

2
SudoKid On

Here is an update of the first 19 lines of code that should improve the performance. In this example all I have done was reduce the total number of times you are preforming math operations.

for y in range(0, 42):
    for x in range(0, 42):
        player_y = player.mapY + y - 21
        player_x = player.mapX + x -21

        if  player_y > 0 and player_y < 128 and player_x > 0 and player_x < 128:
            start_drawing_x_half_graphic_size = startDrawingPosX + x * halfGraphicSizeX - y * halfGraphicSizeX
            start_drawing_y_half_graphic_size = startDrawingPosY + x * halfGraphicSizeY + y * halfGraphicSizeY

            if start_drawing_x_half_graphic_size > -64 and start_drawing_x_half_graphic_size + halfGraphicSizeX < 1024+32 and\
                start_drawing_y_half_graphic_size > -32 and start_drawing_y_half_graphic_size + halfGraphicSizeY < 600+32:

                if self.getGroundAtYX(player.mapY + (y - 21), player.mapX + (x - 21)) is not 0:

                    canvas.create_image(start_drawing_x_half_graphic_size,
                                        start_drawing_y_half_graphic_size,
                                        image=groundGraphics[
                                            self.getGroundAtYX(player.mapY + (y - 21), player.mapX + (x - 21))],
                                        anchor=NW)