Search code examples
python-3.xpygameframe-rate

Why does the display of text decrease my pygame FPS?


I'm trying to write this simple game that replicates a few aspects of Asteroids and have had some problems with the FPS. I have not finished the game yet, but was concerned about the FPS drop as I have a lot more text that I wish to display, but am unable to display for fear the game will suffer so much frame drop. I have only noticed this frame drop with text being displayed on screen. My code can be found below.

import os
import pickle
import pygame
from pygame.locals import *
import random
import sys
import time

pygame.init()
pygame.display.init()

common_drops = ['Janus', 'Peace of Mind', 'Second Chance']
rare_drops = ['Invincibility', 'Shield', 'Bonus Credits', 'Weapon']
ultra_rare_drops = []

janus_count = 0
peace_of_mind_count = 0
bonus_lives_count = 0 #Second Chance
invincibility_count = 0
shield_count = 0
weapon_count = 0
credit_count = 30
high_score = 0

def save():
    loot_out = open('./nec_files/user_data/player.pickle', 'wb')
    pickle.dump(player_loot_data, loot_out)
    loot_out.close()

if os.path.isfile('./nec_files/user_data/player.pickle') == True:

    loot_in = open('./nec_files/user_data/player.pickle', 'rb')
    loot_dict = pickle.load(loot_in)

    player_loot_data = {
        'janus_count' : loot_dict['janus_count'],
        'peace_of_mind_count' : loot_dict['peace_of_mind_count'],
        'bonus_lives_count' : loot_dict['bonus_lives_count'], #Second Chance
        'invincibility_count' : loot_dict['invincibility_count'],
        'shield_count' : loot_dict['shield_count'],
        'weapon_count' : loot_dict['weapon_count'],
        'credit_count' : loot_dict['credit_count'],
        'high_score' : loot_dict['high_score']
    }
    loot_in.close()

    save()

else:

    player_loot_data = {
        'janus_count' : janus_count,
        'peace_of_mind_count' : peace_of_mind_count,
        'bonus_lives_count' : bonus_lives_count, #Second Chance
        'invincibility_count' : invincibility_count,
        'shield_count' : shield_count,
        'weapon_count' : weapon_count,
        'credit_count' : credit_count,
        'high_score' : high_score
    }

    save()

display_width = 1280
display_height = 720

black = (0,0,0)
white = (255,255,255)
blue = (0, 102, 204)
bright_blue = (102, 178, 255)
red = (204, 0, 0)
bright_red = (255, 51, 51)
yellow = (204, 204, 0)
bright_yellow = (255, 255, 102)
gray = (169, 169, 169)

game_title = 'Asteroids: Reimagined'

paused = False
alpha = True

gameDisplay = pygame.display.set_mode((display_width, display_height))
pygame.display.set_caption(game_title)
print(pygame.display.get_driver())
clock = pygame.time.Clock()

playerImg = pygame.image.load('./nec_files/graphics/player_image.png')
asteroidImg = pygame.image.load('./nec_files/graphics/asteroid_image.png')

current_drops = []

def open_common_drop():
    global current_drops

    current_drops.clear()

    if player_loot_data['credit_count'] >= 5:

        player_loot_data['credit_count'] -= 5

        for x in range(3):
            rand_num = random.randint(0, 50)
            if rand_num < 49:
                drops = random.choice(common_drops)
            elif rand_num >= 49:
                drops = random.choice(rare_drops)

            if drops == 'Janus':
                player_loot_data['janus_count'] += 1
            elif drops == 'Peace of Mind':
                player_loot_data['peace_of_mind_count'] += 1
            elif drops == 'Second Chance':
                player_loot_data['bonus_lives_count'] += 1
            elif drops == 'Bonus Credits':
                bonus_credits = random.randint(1, 50)
                player_loot_data['credit_count'] += bonus_credits
            elif drops == 'Invincibility':
                player_loot_data['invincibility_count'] += 1
            elif drops == 'Shield':
                player_loot_data['shield_count'] += 1
            elif drops == 'Weapon':
                player_loot_data['weapon_count'] += 1

            current_drops.append(drops)

        save()

def open_rare_drop():
    global current_drops

    current_drops.clear()

    if player_loot_data['credit_count'] >= 10:

        player_loot_data['credit_count'] -= 10

        for x in range(3):
            rand_num = random.randint(0, 50)
            if rand_num < 36:
                drops = random.choice(common_drops)
            elif rand_num >= 36:
                drops = random.choice(rare_drops)

            if drops == 'Janus':
                player_loot_data['janus_count'] += 1
            elif drops == 'Peace of Mind':
                player_loot_data['peace_of_mind_count'] += 1
            elif drops == 'Second Chance':
                player_loot_data['bonus_lives_count'] += 1
            elif drops == 'Bonus Credits':
                bonus_credits = random.randint(1, 50)
                player_loot_data['credit_count'] += bonus_credits
            elif drops == 'Invincibility':
                player_loot_data['invincibility_count'] += 1
            elif drops == 'Shield':
                player_loot_data['shield_count'] += 1
            elif drops == 'Weapon':
                player_loot_data['weapon_count'] += 1

            current_drops.append(drops)

        save()

def player(player_x, player_y):
    gameDisplay.blit(playerImg, (player_x, player_y))
def asteroid(thingx, thingy):
    gameDisplay.blit(asteroidImg, (thingx, thingy))

def game_display_text(display_msg, display_x, display_y, text_size):
    font = pygame.font.SysFont(None, text_size)
    text = font.render(str(display_msg), True, black)
    gameDisplay.blit(text, (display_x, display_y))

def title(msg):
    largeText = pygame.font.SysFont(None, 75)
    TextSurf, TextRect = text_objects(msg, largeText)
    TextRect.center = ((display_width / 2), (display_height * 0.10))
    gameDisplay.blit(TextSurf, TextRect)

def button(x, y, w, h, ic, ac, action = None):
    global paused

    mouse = pygame.mouse.get_pos()
    click = pygame.mouse.get_pressed()


    if x + w > mouse[0] > x and y + h > mouse[1] > y:
        pygame.draw.rect(gameDisplay, ac, (x, y, w, h))

        if click[0] == 1 and action == Game:
            Game()
        if click[0] == 1 and action == quitgame:
            sys.exit()
        if click[0] == 1 and action == None:
            paused = False
        if click[0] == 1 and action == StartScreen:
            save()
            StartScreen()
        if click[0] == 1 and action == LootScreen:
            LootScreen()
        if click[0] == 1 and action == open_common_drop:
            open_common_drop()
        if click[0] == 1 and action == open_rare_drop:
            open_rare_drop()

    else:
        pygame.draw.rect(gameDisplay, ic, (x, y, w, h))

def things(thingx, thingy, thingw, thingh, color):
    pygame.draw.rect(gameDisplay, color, [thingx, thingy, thingw, thingh])
def things2(thingx, thingy, thingw, thingh, color):
    pygame.draw.rect(gameDisplay, color, [thingx, thingy, thingw, thingh])
def text_box(box_x, box_y, box_w, box_h, color):
    pygame.draw.rect(gameDisplay, color, [box_x, box_y, box_w, box_h])
def text_objects(text, font):
    textSurface = font.render(text, True, black)
    return textSurface, textSurface.get_rect()
def message_display(text):
    largeText = pygame.font.Font('freesansbold.ttf', 50)
    TextSurf, TextRect = text_objects(text, largeText)
    TextRect.center = ((display_width/2),(display_height/2))
    gameDisplay.blit(TextSurf, TextRect)

    pygame.display.update()
    time.sleep(2)

def reset():
    message_display('Out of Bounds: Player Location Reset')

def quitgame():
    pygame.quit()
    sys.exit()

def StartScreen():

    intro = True

    settings_x = 1230
    settings_y = 670

    while intro:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                save()

                pygame.quit()
                sys.exit()

        gameDisplay.fill(gray)
        title(game_title)

        button(420, 250, 125, 50, blue, bright_blue, Game)
        button(720, 250, 125, 50, red, bright_red, quitgame)
        button(570, 250, 125, 50, yellow, bright_yellow, LootScreen)

        game_display_text('Start', 450, 260, 40)
        game_display_text('Quit', 750, 260, 40)
        game_display_text('Loot', 600, 260, 40)

        game_display_text('Licensed by: @1024MBStudio', 925, 690, 35)

        pygame.display.update()
        clock.tick(30)

def LootScreen():
    global current_drops

    loot = True

    while loot:
        for event in pygame.event.get():

            if event.type == pygame.QUIT:
                save()

                pygame.quit()
                sys.exit()

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_t:
                    open_common_drop()
                elif event.key == pygame.K_y:
                    open_rare_drop()

                if event.key == pygame.K_ESCAPE:
                    StartScreen()

        gameDisplay.fill(gray)
        title('Loot Chests!')

        button(400, 150, 260, 50, blue, bright_blue, None)
        button(695, 150, 260, 50, red, bright_red, None)
        button(display_width * 0.42, display_height / 1.15, 255, 50, red, bright_red, StartScreen)

        game_display_text('Open Common Chest (T)', 407, 165, 30)
        game_display_text('Open Rare Chest (Y)', 725, 165, 30)
        game_display_text('You Got: %s' % current_drops, 50, display_height / 2, 35)
        game_display_text('Credits: %.2f' % player_loot_data['credit_count'], 15, 15, 35)
        game_display_text('Main Menu', display_width * 0.47, display_height / 1.13, 35)

        game_display_text('Janus\': %.2f' % player_loot_data['janus_count'] , 1025, 500, 35)
        game_display_text('Peace of Minds: %.2f' % player_loot_data['peace_of_mind_count'], 1025, 535, 35)

        pygame.display.update()
        clock.tick(30)

def PauseScreen():

    global paused

    paused = True

    pausebox_x = 0
    pausebox_y = 625
    pausebox_width = display_width
    pausebox_height = display_height - 625

    while paused:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                save()

                pygame.quit()
                sys.exit()

            if event.type == pygame.KEYDOWN:

                if event.key == pygame.K_ESCAPE:
                    paused = False

        gameDisplay.fill(gray)
        title('Paused')

        button(560, 130, 173, 50, blue, bright_blue, None)
        button(560, 205, 173, 50, red, bright_red, StartScreen)

        game_display_text('Resume', 590, 140, 40)
        game_display_text('Quit', 615, 218, 40)

        text_box(pausebox_x, pausebox_y, pausebox_width, pausebox_height, blue)

        game_display_text('Janus\': %s' % player_loot_data['janus_count'] , 5, 630, 35)
        game_display_text('Peace of Minds: %s' % player_loot_data['peace_of_mind_count'], 5, 665, 35)
        game_display_text('Bonus Lives: %s' % player_loot_data['bonus_lives_count'], 250, 630, 35)

        pygame.display.update()
        clock.tick(30)

def DeadScreen():

    current_score = 0

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                save()

                pygame.quit()
                sys.exit()

        gameDisplay.fill(gray)
        title('You Died')

        game_display_text('You earned %s' % credit_gain + ' credits that game!', display_width * 0.33, display_height * 0.40, 40)

        button(520, 120, 250, 55, blue, bright_blue, Game)
        button(520, 190, 250, 55, red, bright_red, StartScreen)

        game_display_text('Play Again?', 560, 132, 40)
        game_display_text('Main Menu', 569, 205, 40)

        pygame.display.update()
        clock.tick(30)

def Game():
    global death_counter, attempt_counter, credit_gain

    player_x = (display_width * 0.5)
    player_y = (display_height * 0.5)
    player_speed = 5.5
    playerHeight = 50
    x_change = 0
    y_change = 0

    enemyWidth = 165
    thing_startx = 1500
    thing2_startx = 1500
    thing_speed = -6
    thing2_speed = -5.5
    thing_starty = random.randrange(75, display_height - enemyWidth)
    thing2_starty = random.randrange(75, display_height - enemyWidth)

    dead = False
    janus = False
    peace_of_mind = False
    invincibility = False
    full_screen = False

    earnable_credits = 0.125 
    current_score = 0
    credit_gain = 0
    current_lives = 0

    RESETEVENT = pygame.USEREVENT + 1
    DISABLEJANUS = pygame.USEREVENT + 5

    textbox_x = 0
    textbox_y = 0
    textbox_width = 1280
    textbox_height = 60

    while not dead:

        if pygame.display.get_active() == True:
            for event in pygame.event.get():
                pygame.time.set_timer(RESETEVENT, 275)

                if peace_of_mind == True:
                    thing_original_speed = thing_speed
                    thing2_original_speed = thing2_speed

                if event.type == RESETEVENT:
                    current_score += 1
                    pygame.time.set_timer(RESETEVENT, 275)
                if event.type == DISABLEJANUS:
                    janus = False
                if event.type == pygame.QUIT:
                    save()

                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if janus == True:
                        if event.key == pygame.K_LEFT or event.key == pygame.K_a:
                            x_change = -player_speed
                        elif event.key == pygame.K_RIGHT or event.key == pygame.K_d:
                            x_change = player_speed
                    if event.key == pygame.K_UP or event.key == pygame.K_w:
                        y_change = -player_speed
                    elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
                        y_change = player_speed

                    if event.key == pygame.K_u and player_loot_data['janus_count'] > 0 and janus == False:
                        pygame.time.set_timer(DISABLEJANUS, 15000)
                        player_loot_data['janus_count'] -= 1
                        janus = True
                    elif event.key == pygame.K_i and player_loot_data['peace_of_mind_count'] > 0 and peace_of_mind == False:
                        player_loot_data['peace_of_mind_count'] -= 1
                        peace_of_mind = True
                    elif event.key == pygame.K_o and player_loot_data['bonus_lives_count'] > 0:
                        player_loot_data['bonus_lives_count'] -= 1
                        current_lives += 1

                    if event.key == pygame.K_ESCAPE:
                        PauseScreen()
                    elif event.key == pygame.K_F4:
                        sys.exit()
                    elif event.key == pygame.K_F11:
                        if full_screen == False:
                            pygame.display.set_mode((display_width, display_height), pygame.FULLSCREEN)
                            full_screen = True
                            PauseScreen()
                        elif full_screen == True:
                            pygame.display.set_mode((display_width, display_height))
                            PauseScreen()

                elif 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:
                        x_change = 0
                    elif event.key == pygame.K_UP or event.key == pygame.K_DOWN or event.key == pygame.K_w or event.key == pygame.K_s:
                        y_change = 0
                    elif event.key == pygame.K_SPACE:
                        y_change = 0

            if thing_startx < 0 - enemyWidth:
                thing_startx = 1500
                thing_starty = random.randrange(75, display_height - enemyWidth)
                thing_speed += -0.05
                player_loot_data['credit_count'] += earnable_credits
                credit_gain += earnable_credits

            if thing2_startx < 0 - enemyWidth:
                thing2_startx = 1500
                thing2_starty = random.randrange(75, display_height - enemyWidth)
                thing2_speed += -0.1
                player_loot_data['credit_count'] += earnable_credits
                credit_gain += earnable_credits

            player_x += x_change
            player_y += y_change
            thing_startx += thing_speed
            thing2_startx += thing2_speed

            if player_loot_data['high_score'] < current_score:
                player_loot_data['high_score'] = current_score

            if player_y > display_height:
                player_y = textbox_height
            if player_y < 10:
                player_y = display_height - playerHeight
            if player_x < 0 - playerHeight:
                player_x = (display_width * 0.5)
            if player_x > display_width:
                player_x = (display_width * 0.5)


            if player_y < thing_starty + enemyWidth and player_y + playerHeight > thing_starty:

                if player_x > thing_startx and player_x < thing_startx + enemyWidth or player_x + playerHeight > thing_startx and player_x + playerHeight < thing_startx + enemyWidth:

                    if current_lives > 0:
                        current_lives -= 1

                        player_x = (display_width * 0.5)
                        player_y = (display_height * 0.5)

                        thing_startx = 1500
                        thing2_startx = 1500                    

                    else:
                        dead = True

            if player_y < thing2_starty + enemyWidth and player_y + playerHeight > thing2_starty:

                if player_x > thing2_startx and player_x < thing2_startx + enemyWidth or player_x + playerHeight > thing2_startx and player_x + playerHeight < thing2_startx + enemyWidth:

                    if current_lives > 0:
                        current_lives -= 1

                        player_x = (display_width * 0.5)
                        player_y = (display_height * 0.5)

                        thing_startx = 1500
                        thing2_startx = 1500

                    else:
                        dead = True

            else:
                crossover = 'null'

            gameDisplay.fill(gray)

            player(player_x, player_y)
            asteroid(thing_startx, thing_starty)
            asteroid(thing2_startx, thing2_starty)

            text_box(textbox_x, textbox_y, textbox_width, textbox_height, blue)

            game_display_text('High Score: %s' % player_loot_data['high_score'], 5, 5, 30)
            game_display_text('Current Score: %s' % current_score, 5, 35, 30)
            game_display_text('Current Chances: %s' % current_lives, 200, 5, 30)

            if janus == True:
                game_display_text('Janus Enabled', 850, 5, 30)
            if peace_of_mind == True:
                game_display_text('Peace of Mind Enabled', 850, 35, 30)
            if invincibility == True:
                game_display_text('Invincibility Enabled', 950, 5, 30)

            if alpha == True:
                game_display_text('FPS: %s' % clock.get_fps(), 5, 635, 30)

            pygame.display.update()
            clock.tick()
        else:
            PauseScreen()

    DeadScreen()

if __name__ == '__main__':
    StartScreen()
    sys.exit()

Solution

  • You're doing two things wrong with your text rendering.

    The first (and probably major) one is that you load the font again and again every time you want to display some text:

    def game_display_text(display_msg, display_x, display_y, text_size):
        font = pygame.font.SysFont(None, text_size)
        ...
    

    You should create the font object once, so you don't load it every time from disk.

    The second issue is that the rendering of the text to a Surface is a rather expensive operation:

    def game_display_text(display_msg, display_x, display_y, text_size):
        ...
        text = font.render(str(display_msg), True, black)
        ...
    

    A better method is to cache the already created surfaces, and reuse them.

    A very simple cache could look like this:

    text_font = pygame.font.SysFont("whatever", 14)
    cache={}
    def get_msg(msg):
        if not msg in cache:
          cache[msg] = text_font.render(msg, 1 , text_color)
        return cache[msg]
    

    and then you would use the get_msg method to create your text surfaces. Or use something like e.g. the lru_cache decorator.