1
\$\begingroup\$

I'm currently working on a very simple RPG game but when I try to load all the tile images for the background, the game lags to the point it freezes (framerate drops to 0 or will just crash). The tiles are 40x40 .png images and are contained in the tiles folders.

The game is divided in 3 files:

Structure of the project:

enter image description here

main.py:

import pygame
import sys
import time
from textures import *
from globalsvar import *

pygame.init()

cSecs = 0
cFrame = 0
fps = 0
black = (0, 0, 0)

map_data = []

for x in range(0, 32):
    for y in range(0, 24):
        map_data.append([x, y, "2"])

clock = pygame.time.Clock()

tile_size = 25

info_font = pygame.font.SysFont("Verdana", 20)


class Mage:
    def __init__(self, location):
        self.x, self.y = location
        self.name = "Character"
        self.image = pygame.image.load("graphics\\sprites\\ninja_left.png")
        self.x_update = 0
        self.y_update = 0
        self.mage_width_dif = win_width - self.image.get_width()
        self.mage_height_dif = win_height - self.image.get_height()

    def draw_self(self):
        window.blit(self.image, (self.x, self.y))

    def update(self):
        self.x += self.x_update
        self.y += self.y_update


def create_window():
    global window, win_height, win_width, win_title
    win_width, win_height = 800, 600
    win_title = "A Little RPG Game"
    pygame.display.set_caption(win_title)
    window = pygame.display.set_mode((win_width, win_height), pygame.HWSURFACE|pygame.DOUBLEBUF)


def draw_grid_terrain():
for x in range(-900, 1800, tile_size):
    for y in range(-900, 1800, tile_size):
        for i in map_data:
            tile = (i[0]*Tiles.File_size, i[1]*Tiles.File_size)
            if (x, y) == tile:
                window.blit(Tiles.Texture_Tags[i[2]], (x + Globals.camera_X, y + Globals.camera_Y))


def show_fps():
    fps_overlay = info_font.render(str(fps), True, black)
    window.blit(fps_overlay, (10, 10))


def fps_manager():
    global cFrame, fps, cSecs, deltaTime

    if cSecs == time.strftime("%S"):
        cFrame += 1
    else:
        fps = cFrame
        cFrame = 0
        cSecs = time.strftime("%S")


gameRunning = True

create_window()

character = Mage((win_width/2, win_height/2))

while(gameRunning):

    if character.x + character.x_update >= character.mage_width_dif or character.x + character.x_update <= 0:
        character.x -= character.x_update

    if character.y + character.y_update >= character.mage_height_dif or character.y + character.y_update <= 0:
            character.y -= character.y_update

    if character.y > ((75 * win_height)/100):
        Globals.camera_MOVE = 1
    elif character.y < ((5 * win_height)/100):
        Globals.camera_MOVE = 2
    elif character.x > ((75 * win_width)/100):
        Globals.camera_MOVE = 3
    elif character.x < ((5 * win_width)/100):
        Globals.camera_MOVE = 4
    else:
        Globals.camera_MOVE = 0

    if Globals.camera_MOVE == 1:
        Globals.camera_Y -= 3.5

    elif Globals.camera_MOVE == 2:
        Globals.camera_Y += 3.5

    elif Globals.camera_MOVE == 3:
        Globals.camera_X -= 3.5

    elif Globals.camera_MOVE == 4:
        Globals.camera_X += 3.5


    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            gameRunning = False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == pygame.K_a:
                character.x_update = -1
            elif event.key == pygame.K_RIGHT or event.key == pygame.K_d:
                character.x_update = 1
            elif event.key == pygame.K_UP or event.key == pygame.K_w:
                character.y_update = -1
            elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
                character.y_update = 1

        if event.type == pygame.KEYUP:
            if (event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT
                    or event.key == pygame.K_a or event.key == pygame.K_d):
                character.x_update = 0
            elif (event.key == pygame.K_DOWN or event.key == pygame.K_UP
                  or event.key == pygame.K_w or event.key == pygame.K_s):
                character.y_update = 0

    clock.tick(60)
    draw_grid_terrain()
    character.update()
    character.draw_self()
    fps_manager()
    show_fps()
    pygame.display.update()

pygame.quit()
sys.exit()

textures.py:

import pygame

pygame.init()


class Tiles:
    File_size = 25

    def load_textures(file, size):
        bitmap = pygame.image.load(file)
        bitmap = pygame.transform.scale(bitmap, (size, size))
        surface = pygame.Surface((size, size), pygame.HWSURFACE|pygame.SRCALPHA)
        surface.blit(bitmap, (0, 0))
        return surface

    Grass = load_textures("graphics\\tiles\\grass.png", File_size)
    Ground = load_textures("graphics\\tiles\\ground.png", File_size)
    Water = load_textures("graphics\\tiles\\water.png", File_size)
    Floor = load_textures("graphics\\tiles\\floor.png", File_size)

    Texture_Tags = {"1": Grass, "2": Ground, "3": Water, "4": Floor}

globalsvar.py:

class Globals:
        camera_X = 0
        camera_Y = 0
        camera_MOVE = 0
        camera_pos_X = 0
        camera_pos_Y = 0

Game screenshot:

enter image description here

I read somewhere that I should try loading all the tiles on a surface once and then display said surface, but I don't know how.

Why is my game lagging so much, and how could I improve the framerate?

\$\endgroup\$
10
  • 1
    \$\begingroup\$ And what is your question? This is essentially a statement not a question. And what have you tried? You claimed it sometimes crashes. What is the crash log? \$\endgroup\$
    – Charanor
    Commented May 2, 2017 at 12:28
  • 1
    \$\begingroup\$ Do you load the tiles every frame? \$\endgroup\$
    – Bálint
    Commented May 2, 2017 at 12:30
  • 1
    \$\begingroup\$ @conq_rp That's bad, IO is slow, you should only load the textures once \$\endgroup\$
    – Bálint
    Commented May 2, 2017 at 14:13
  • 1
    \$\begingroup\$ Have you profiled the game? That's the first thing you should always do when you're having performance issues. \$\endgroup\$
    – user35344
    Commented May 2, 2017 at 14:24
  • 1
    \$\begingroup\$ One thing that could help performance for those ground tiles would be to use a tool to tile them ahead of time and save them as one image. There is a good tool called Tiled that is free that I have use to do this. \$\endgroup\$ Commented May 2, 2017 at 16:36

1 Answer 1

1
\$\begingroup\$

You should (almost) always call convert() or convert_alpha() after loading an image to improve the performance.

So instead of pygame.image.load(file) do pygame.image.load(file).convert() or if the image has transparent parts pygame.image.load(file).convert_alpha().

And as mentioned in one of the comments, only load the images once at the program start and then reuse them, because it's slow to load them from the hard drive each frame. Edit: Actually, you're not loading the images again and again, because you load them as class attributes which are evaluated at the program start. But this is still important, so I don't remove this recommendation.


The suggestions above are just a drop in the ocean, since the main problem is pretty clearly (without profiling) the draw_grid_terrain function. It has to iterate 8957952 times per frame.

It's a lot faster to use big surfaces instead of blitting small tiles the whole time. Here's an example in which I first create a big surface and blit the tiles on it (of course you can also create your map in Tiled and then load it in pygame).

I also tried to simplify the rest of the code, especially for the camera. I gave the Mage a rect and some vectors which serve to store the position, velocity and camera pos. To check if the player is in the camera area, I also gave him an area rect, and if he's outside I use Rect.clamp_ip to keep him in this area and move the camera vector.

import sys
import pygame as pg


class Mage:

    def __init__(self, location):
        self.image = pg.Surface((TILE_SIZE, TILE_SIZE*2))
        self.image.fill((40, 110, 180))
        self.rect = self.image.get_rect(topleft=location)
        self.pos = pg.math.Vector2(location)
        self.vel = pg.math.Vector2(0, 0)
        self.camera = pg.math.Vector2(0, 0)
        # The area where the player can move without affecting the camera.
        self.area = pg.Rect(100, 100, 600, 400)

    def draw_self(self, window):
        window.blit(self.image, self.rect)

    def update(self):
        """Move the player and the camera."""
        self.pos += self.vel  # Add the vel vector to pos to move.
        # Update the rect, since it's the blit position and is needed to
        # check if the camera should be moved.
        self.rect.topleft = self.pos
        rect = self.rect
        area = self.area
        # If the player has moved outside of the area rect ...
        if rect.left < self.area.left or rect.right > self.area.right:
            self.camera.x -= self.vel.x  # ... move camera.x.
        # The same for the y direction.
        if rect.top < self.area.top or rect.bottom > self.area.bottom:
            self.camera.y -= self.vel.y

        self.rect.clamp_ip(self.area)  # Keep the player in the area.
        self.pos = self.rect.topleft  # Adjust pos after the clamp.


def draw_grid_terrain(surface, map_data):
    """Blit the map tiles on the surface."""
    for row in map_data:
        for rect, tile_type in row:
            surface.blit(TEXTURES[tile_type], rect.topleft)


TILE_SIZE = 32
# The small surfaces that will be blitted on the big surface below.
GRASS = pg.Surface((TILE_SIZE, TILE_SIZE))
GRASS.fill((40, 140, 20))
pg.draw.rect(GRASS, (10, 100, 50), (0, 0, TILE_SIZE, TILE_SIZE), 2)
GROUND = pg.Surface((TILE_SIZE, TILE_SIZE))
GROUND.fill((90, 40, 20))
pg.draw.rect(GROUND, (110, 100, 50), (0, 0, TILE_SIZE, TILE_SIZE), 2)
WATER = pg.Surface((TILE_SIZE, TILE_SIZE))
WATER.fill((40, 70, 170))
pg.draw.rect(WATER, (80, 80, 190), (0, 0, TILE_SIZE, TILE_SIZE), 2)
FLOOR = pg.Surface((TILE_SIZE, TILE_SIZE))
FLOOR.fill((120, 120, 100))
pg.draw.rect(FLOOR, (20, 20, 20), (0, 0, TILE_SIZE, TILE_SIZE), 2)

TEXTURES = {'1': GRASS, '2': GROUND, '3': WATER, '4': FLOOR}

# The map is a list of lists which contain pg.Rects and the tile_type.
MAP_DATA = [[(pg.Rect(TILE_SIZE*x, TILE_SIZE*y, TILE_SIZE, TILE_SIZE), '2')
             for x in range(128)]
            for y in range(128)]
MAP_DATA[4] = [(rect, '1') for rect, _ in MAP_DATA[4]]  # A grass row.
MAP_DATA[20] = [(rect, '3') for rect, _ in MAP_DATA[20]]  # Water.
MAP_DATA[27] = [(rect, '4') for rect, _ in MAP_DATA[27]]  # Floor.
# Create a big surface with the size of the map.
BACKGROUND = pg.Surface((TILE_SIZE*128, TILE_SIZE*128))
draw_grid_terrain(BACKGROUND, MAP_DATA)  # Blit the tiles on the image.


def main():
    clock = pg.time.Clock()
    width, height = 800, 600
    window = pg.display.set_mode((width, height))
    character = Mage((width/2, height/2))
    game_running = True

    while game_running:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                game_running = False
            if event.type == pg.KEYDOWN:
                if event.key in (pg.K_LEFT, pg.K_a):
                    character.vel.x = -3
                elif event.key in (pg.K_RIGHT, pg.K_d):
                    character.vel.x = 3
                elif event.key in (pg.K_UP, pg.K_w):
                    character.vel.y = -3
                elif event.key in (pg.K_DOWN, pg.K_s):
                    character.vel.y = 3
            if event.type == pg.KEYUP:
                if event.key in (pg.K_LEFT, pg.K_RIGHT, pg.K_a, pg.K_d):
                    character.vel.x = 0
                elif event.key in (pg.K_DOWN, pg.K_UP, pg.K_w, pg.K_s):
                    character.vel.y = 0

        character.update()
        # Just blit the background image instead of the single tiles.
        # Adjust its position by the character's camera vector.
        window.blit(BACKGROUND, (0, 0)+character.camera)
        character.draw_self(window)
        pg.display.set_caption('FPS: {}'.format(clock.get_fps()))
        pg.display.update()
        clock.tick(60)

if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()
    sys.exit()
\$\endgroup\$
1
  • \$\begingroup\$ I've started to rewrite the program completely, so it'll take some more time till I post an example. The best way to improve the performance is to combine the single tiles into one or more big images (pygame.Surfaces) and then just blit them once per frame. Tiled could be useful as someone else suggested. \$\endgroup\$
    – skrx
    Commented May 16, 2017 at 22:22

You must log in to answer this question.

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