Search code examples
pythonpython-2.7pygamepycharmframe-rate

Python pygame how to set the FPS


I am making a python game and I don't know what should I set my FPS to. My game is stuck and not smooth. How do I know what should the FPS be?

This is my code:

http://bin.shortbin.eu:8080/x87bSUKUiN


Solution

  • After running your code I have also noticed frame rate drops which hurt the smoothness of the game.

    There are two separate issues here:

    1. The FPS drops

    The FPS drops probably happen because of something you cannot control, like the garbage collector working. Even though you have no control of such issues, you can in general improve the performance of your game. See the following screen shot of a profiler run of your game: Profiler results

    You can see that the largest part of the time is spent on blit. However, a very large part of the time is also being spent in get_y_list. The get_y_list method also uses large lists which create a lot of garbage for the garbage collector to later collect, thus causing a further performance hit.

    As I understand it, the get_y_list method is part of a very crude method that you're using for collision detection, one which basically takes quadratic time. Your algorithm seems to divide each object into a large amount of 2d cells, and then test collision between each pair of cells. Instead you can just test box to box intersection. If you wish to have a complicated collision shape for the objects you can use other algorithms, the internet is full of them. See this for example: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection

    Using other algorithms for collision detection will greatly improve your performance.

    2. The game becoming non-smooth when there's an FPS drop.

    The x and y positions of your objects are being updated like this for example: player.x -= player.vx. The physically correct method would be: player.x -= player.vx * DELTA_T. Where DELTA_T is the time elapsed since the last frame. If you don't use the time elapsed since the last frame, the speed of motion of your characters becomes dependent on FPS. Which is exactly what we're seeing.

    Where do I get DELTA_T you might ask. You do it directly when calling tick:

    def main():
        global DELTA_T
    
        finish = False
        background_x = 0
        background_y = 0
        background = pygame.image.load(BACKGROUND_IAMGE)
    
        while not finish:
            DELTA_T = clock.tick(REFRESH_RATE)
    

    I tried adding the multiplication by DELTA_T but the game becomes unstable, because you assume that objects advance exactly vx 'cells' in each frame to detect collisions and floor penetrations. See my attempt below:

    import pygame
    
    pygame.init()
    WINDOW_WIDTH = 700
    WINDOW_HEIGHT = 500
    SIZE = (WINDOW_WIDTH, WINDOW_HEIGHT)
    SCREEN = pygame.display.set_mode(SIZE)
    pygame.display.set_caption("Python Game")
    WHITE = (255, 255, 255)
    RED = (255, 0, 0)
    GREEN = (0, 255, 0)
    BLUE = (0, 0, 255)
    PINK = (255, 0, 255)
    BACKGROUND_IAMGE = 'back.png'
    clock = pygame.time.Clock()
    REFRESH_RATE = 30
    FRAMES_DELAY = 3
    
    PLAYER_HEIGHT = 72
    PLAYER_WIDTH = 40
    
    MUSHROOM_HEIGHT = 31
    MUSHROOM_WIDTH = 40
    
    BLOCK_HEIGHT = 30
    BLOCK_WIDTH = 30
    
    FLOOR_Y = 435
    
    DELTA_T = 0
    
    class Player:
        def __init__(self, x, y):
            self.__images = [pygame.image.load('mario1.png'), pygame.image.load('mario2.png'),
                             pygame.image.load('mario3.png'), pygame.image.load('mario4.png'),
                             pygame.image.load('mario5.png'), pygame.image.load('mario6.png')]
            self.__current_image = self.__images[0]
            self.x = x
            self.y = y
            self.vx = 5/33.
            self.vy = 10/33.
            self.__is_mid_air = False
            self.__direction = "right"
            self.__is_jumping = False
            self.__MAX_JUMP = 20
            self.__is_walking = False
            self.__counter = 0
            self.__jump_counter = 0
    
        def reset_jump_counter(self):
            self.__jump_counter = 0
    
        def get_bottom_y(self):
            return int(self.y + PLAYER_HEIGHT)
    
        def set_walk(self, bol):
            self.__is_walking = bol
    
        def set_is_mid_air(self, bol):
            self.__is_mid_air = bol
    
        def is_mid_air(self):
            return self.__is_mid_air
    
        def get_image(self):
            if self.__is_mid_air and self.__direction == "right":
                self.__current_image = self.__images[4]
                return self.__current_image
    
            if self.__is_mid_air and self.__direction == "left":
                self.__current_image = self.__images[5]
                return self.__current_image
    
            self.__counter += 1
            if self.__counter > FRAMES_DELAY:
                self.__counter = 0
    
            if self.__counter == FRAMES_DELAY and self.__direction == "right":
                if self.__is_walking and self.__current_image == self.__images[0]:
                    self.__current_image = self.__images[1]
                else:
                    self.__current_image = self.__images[0]
    
            if self.__counter == FRAMES_DELAY and self.__direction == "left":
                if self.__is_walking and self.__current_image == self.__images[2]:
                    self.__current_image = self.__images[3]
                else:
                    self.__current_image = self.__images[2]
    
            return self.__current_image
    
        def stand_still(self):
            if self.__direction == "right":
                self.__current_image = self.__images[0]
            if self.__direction == "left":
                self.__current_image = self.__images[2]
    
        def set_jump(self, bol):
            self.__is_jumping = bol
            if not bol:
                self.reset_jump_counter()
    
        def check_jump(self):
            if self.__jump_counter != self.__MAX_JUMP and self.__is_jumping:
                self.y -= self.vy * DELTA_T
                self.__jump_counter += 1
            if self.__jump_counter >= self.__MAX_JUMP:
                self.__is_jumping = False
                self.__jump_counter = 0
    
        def is_jumping(self):
            return self.__is_jumping
    
        def fall(self):
            if not self.__is_jumping:
                self.y += self.vy * DELTA_T
    
        def get_direction(self):
            return self.__direction
    
        def turn_around(self):
            if self.__direction == "right":
                self.__direction = "left"
            elif self.__direction == "left":
                self.__direction = "right"
    
        def get_x_list(self):
            result = []
            for i in range(PLAYER_WIDTH + 1):
                result.append(self.x + i)
            return result
    
        def get_y_list(self):
            result = []
            for i in range(PLAYER_HEIGHT + 1):
                result.append(self.y + i)
            return result
    
        def get_right_x(self):
            return self.x + PLAYER_WIDTH
    
        def is_crash(self, obj):
            is_x_equals = False
            for i in range(int(self.vx * DELTA_T+0.5)):
                if self.x + PLAYER_WIDTH + i in obj.get_x_list():
                    is_x_equals = True
                if self.x - i in obj.get_x_list():
                    is_x_equals = True
    
            is_y_equals = False
            for i in range(int(self.vy*DELTA_T+0.5)):
                if self.y + PLAYER_HEIGHT + i in obj.get_y_list():
                    is_y_equals = True
                if self.y - i in obj.get_y_list():
                    is_y_equals = True
    
            return is_x_equals and is_y_equals
    
    class Monster:
        def __init__(self, x, y, vx, vy, monster_type):
            self.__images = [pygame.image.load(monster_type + '1.png').convert(),
                             pygame.image.load(monster_type + '2.png').convert()]
            if monster_type == "mushroom":
                self.__width = MUSHROOM_WIDTH
                self.__height = MUSHROOM_HEIGHT
    
            self.__current_image = self.__images[0]
            self.__FIRST_X = x
            self.__FIRST_Y = y
            self.__x = x
            self.__y = y
            self.__vx = vx
            self.__vy = vy
            self.__direction = "left"
            self.__monster_type = monster_type
            self.__counter = 0
    
        def get_image(self):
            self.__counter += 1
            if self.__counter > FRAMES_DELAY:
                self.__counter = 0
    
            if self.__monster_type == "mushroom" and self.__counter == FRAMES_DELAY:
                if self.__current_image == self.__images[1]:
                    self.__current_image = self.__images[0]
                    self.__current_image.set_colorkey(PINK)
                else:
                    self.__current_image = self.__images[1]
                    self.__current_image.set_colorkey(PINK)
                return self.__current_image
            elif self.__monster_type == "mushroom" and self.__counter != FRAMES_DELAY:
                self.__current_image.set_colorkey(PINK)
                return self.__current_image
    
        def get_x(self):
            return self.__x
    
        def get_vx(self):
            return self.__vx
    
        def get_vy(self):
            return self.__vx
    
        def get_y(self):
            return self.__y
    
        def step(self):
            if self.__direction == "right":
                self.__x += self.__vx * DELTA_T
            elif self.__direction == "left":
                self.__x -= self.__vx * DELTA_T
    
        def get_direction(self):
            return self.__direction
    
        def turn_around(self):
            if self.__direction == "right":
                self.__direction = "left"
            elif self.__direction == "left":
                self.__direction = "right"
    
        def get_x_list(self):
            result = []
            for i in range(MUSHROOM_WIDTH + 1):
                result.append(self.__x + i)
            return result
    
        def get_y_list(self):
            result = []
            for i in range(MUSHROOM_HEIGHT + 1):
                result.append(self.__y + i)
            return result
    
        def is_crash(self, obj):
            is_x_equals = False
            for i in range(int(self.__vx * DELTA_T+0.5)):
                if self.__x + self.__width + i in obj.get_x_list():
                    is_x_equals = True
                if self.__x - i in obj.get_x_list():
                    is_x_equals = True
    
            is_y_equals = False
            for i in range(int(self.__vy * DELTA_T+0.5)):
                if self.__y + self.__height + i in obj.get_y_list():
                    is_y_equals = True
                if self.__y - i in obj.get_y_list():
                    is_y_equals = True
    
            return is_x_equals and is_y_equals
    
    class Block:
        def __init__(self, x, y, width=1, height=1):
            self.__image = pygame.image.load("block.png")
            self.__x = x
            self.__y = y
            self.__width = width
            self.__height = height
    
        def load_image(self, background_x):
            for i in range(self.__width):
                for j in range(self.__height):
                    SCREEN.blit(self.__image, (self.__x + (i * BLOCK_WIDTH) + background_x, self.__y + (j * BLOCK_HEIGHT)))
    
        def get_x_list(self):
            result = []
            for i in range(BLOCK_WIDTH * self.__width + 1):
                result.append(self.__x + i)
            return result
    
        def get_y_list(self):
            result = []
            for i in range(BLOCK_HEIGHT * self.__height + 1):
                result.append(self.__y + i)
            return result
    
        def get_y(self):
            return self.__y
    
        def get_x(self):
            return self.__x
    
        def get_width(self):
            return self.__width
    
        def get_height(self):
            return self.__height
    
        def get_bottom_y(self):
            return self.__y + (BLOCK_HEIGHT * self.__height)
    
        def get_right_x(self):
            return self.__x + self.__width * BLOCK_WIDTH
    
    player = Player(140, FLOOR_Y - PLAYER_HEIGHT)
    blocks = [Block(270, 280, 1, 1), Block(301, FLOOR_Y - BLOCK_HEIGHT * 8, 1, 8),
              Block(600, FLOOR_Y - BLOCK_HEIGHT * 8, 2, 8)]
    monsters = [Monster(380, FLOOR_Y - MUSHROOM_HEIGHT, 3/33., 3/33., "mushroom")]
    
    def main():
        global DELTA_T
    
        finish = False
        background_x = 0
        background_y = 0
        background = pygame.image.load(BACKGROUND_IAMGE)
    
        while not finish:
            DELTA_T = clock.tick(REFRESH_RATE)
            SCREEN.blit(background, (background_x, background_y))
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    finish = True
    
            block_on_right = False
            block_on_left = False
            is_player_on_block = False
            for block in blocks:
                block.load_image(background_x)
                for i in range(int(player.vy * DELTA_T+0.5)):
                    for x in player.get_x_list():
                        if player.get_bottom_y() + i == block.get_y() and x in block.get_x_list():
                            is_player_on_block = True
                            player.y += i
                        if player.y - i == block.get_bottom_y() and x in block.get_x_list():
                            player.set_jump(False)
                            player.y -= i
    
                for i in range(int(player.vx*DELTA_T+0.5)):
                    for y in player.get_y_list():
                        if player.get_right_x() + i == block.get_x() and y in block.get_y_list():
                            block_on_right = True
                            player.x += (i - 1)
                            background_x -= (i - 1)
                        if player.x - i == block.get_right_x() and y in block.get_y_list():
                            block_on_left = True
                            player.x -= (i - 1)
                            background_x += (i - 1)
                for monster in monsters:
                    if monster.is_crash(block):
                        monster.turn_around()
    
            keys = pygame.key.get_pressed()
    
            if (keys[pygame.K_UP] or keys[pygame.K_SPACE] or keys[pygame.K_w]) and not player.is_mid_air():
                player.set_jump(True)
    
            if (keys[pygame.K_RIGHT] or keys[pygame.K_d]) and not block_on_right:
                if player.get_direction() != "right":
                    player.turn_around()
                player.set_walk(True)
    
                background_x -= player.vx * DELTA_T
                player.x += player.vx * DELTA_T
    
            if (keys[pygame.K_LEFT] or keys[pygame.K_a]) and not block_on_left:
                if player.get_direction() != "left":
                    player.turn_around()
                player.set_walk(True)
    
                if background_x != 0:
                    background_x += player.vx * DELTA_T
                    player.x -= player.vx * DELTA_T
    
            if not any(keys):
                player.stand_still()
                player.set_walk(False)
    
            for monster in monsters:
                monster.step()
                SCREEN.blit(monster.get_image(), (background_x + monster.get_x(), monster.get_y()))
    
            is_player_on_ground = False
            for i in range(int(player.vy * DELTA_T+0.5)):
                if player.get_bottom_y() + i == FLOOR_Y:
                    player.y += i
                    is_player_on_ground = True
    
            if is_player_on_block or is_player_on_ground:
                player.set_is_mid_air(False)
            else:
                player.set_is_mid_air(True)
                player.fall()
    
            player.check_jump()
    
            player_image = player.get_image().convert()
            player_image.set_colorkey(PINK)
    
            SCREEN.blit(player_image, (player.x + background_x, player.y))
            pygame.display.flip()
    
        pygame.quit()
    
    if __name__ == '__main__':
        main()