Weird error when trying to optimize my game in Pygame

Question:

So I’m making a game in Pygame and I made a system that spawns a bunch of trees and rocks. These are objects that call an update() function where some necessary calculations for their position are made and they get blited to the screen. I’m trying to make a program that only really updates a foliage object when its visible on the screen, So I made a function that returns if its onscreen.

So I added an if statement that runs this function and suddenly I get 600 FPS average but for some reason the player is moving very slowly and the animation goes very fast. I use a delta time system which might have something to do with it but I’m not certain.

Here is the GitHub repository:

import pygame
from pygame.locals import *
from random import randint, choice
import time

screen_width = 800#int(1500/1.5)
screen_height = 800#int(1500/1.5)

flags =  HWSURFACE|DOUBLEBUF|NOFRAME   #| FULLSCREEN

screen = pygame.display.set_mode((screen_width, screen_height), flags, 1000000000)

fps = 0
clock = pygame.time.Clock()
last_time = time.time()
time_now = last_time
dt = 0

images = {
    "players" : {
        "base_right" : [
            pygame.image.load("images/player/baseplr1.png").convert_alpha(),
            pygame.image.load("images/player/baseplr2.png").convert_alpha(),
            ],
        "base_left" : [
            pygame.transform.flip(pygame.image.load("images/player/baseplr1.png").convert_alpha(),True, False),
            pygame.transform.flip(pygame.image.load("images/player/baseplr2.png").convert_alpha(),True, False),
            ],
    },
    "enemies" : {
        "simple" : [
            pygame.image.load("images/enemies/zombie.png").convert_alpha()
        ]
    },
    "foliage" : {
        "tree" : pygame.image.load("images/foliage/tree.png").convert_alpha() ,
        "rock" : pygame.image.load("images/foliage/rock.png").convert_alpha() ,
    },

    "background" : pygame.transform.scale(pygame.image.load("background.png").convert_alpha(),(1000,1000)),

}

class Player(pygame.sprite.Sprite):
    def __init__(self, pos, stats, skin, ori):
        pygame.sprite.Sprite.__init__(self)

        self.dmg = stats[0]
        self.hp = stats[1]
        self.speed = stats[2]

        self.skin = skin

        self.ori = ori
        self.index_max = len(images["players"][self.skin+"_"+self.ori])
        self.index = 0
        self.image = images["players"][self.skin+"_"+self.ori][self.index]

        self.count = False
        self.timer = 0
        self.anim_max = 16

        self.rect = self.image.get_rect(center = pos)

        self.x, self.y = self.rect.x, self.rect.y

    def update(self):
        self.movement()
        self.animate()

    def movement(self):
        self.keys = pygame.key.get_pressed()

        self.count = False

        if self.keys[K_d]:
            self.x += self.speed * dt
            self.ori = "right"
            self.count = True
        if self.keys[K_a]:
            self.x -= self.speed * dt
            self.ori = "left"
            self.count = True

        if self.keys[K_s]:
            self.y += self.speed* dt
            self.count = True
        if self.keys[K_w]:
            self.y -= self.speed * dt
            self.count = True

        self.rect.x = self.x - main_cam.scroll[0]
        self.rect.y = self.y - main_cam.scroll[1]

    def animate(self):
        if self.count:
            self.timer += 1

            if self.timer >= self.anim_max:
                self.timer = 0
                self.index += 1
                if self.index == self.index_max:
                    self.index = 0

        self.image = images["players"][self.skin+"_"+self.ori][self.index]

class Foliage:
    def __init__(self, pos, type):
        self.image = images["foliage"][type]
        self.rect = self.image.get_rect()
        self.x = pos[0]
        self.y = pos[1]
    def update(self):
        self.rect.x = self.x - main_cam.scroll[0]
        self.rect.y = self.y - main_cam.scroll[1]
        screen.blit(self.image, (self.rect.x,self.rect.y))

    def on_screen(self):
        return self.rect.x > 0 and self.rect.x < screen_width and self.rect.y > 0 and self.rect.y < screen_width

class Camera:
    def __init__(self, speed):
        self.scroll = [5,5]
        self.speed = speed
    def move_on_command(self):
        keys = pygame.key.get_pressed()

        if keys[K_UP]:
            self.scroll[1] -= self.speed * dt
        if keys[K_DOWN]:
            self.scroll[1] += self.speed * dt
        if keys[K_RIGHT]:
            self.scroll[0] += self.speed * dt
        if keys[K_LEFT]:
            self.scroll[0] -= self.speed * dt

    def follow(self, obj, speed=12):

        if (obj.x - self.scroll[0]) != screen.get_width()/2:
            self.scroll[0] += ((obj.x - (self.scroll[0] + screen.get_width()/2))/speed ) * dt
        if obj.y - self.scroll[1] != screen.get_height()/2:
            self.scroll[1] += ((obj.y - (self.scroll[1] + screen.get_height()/2))/speed) * dt

class Folliage_Manager:
    def __init__(self, multi=1):
        self.foliage = []
        self.foliage_amount = multi

    def spawn_foliage(self, amount):
        for i in range(int(amount*self.foliage_amount)):
            cord = (randint(-10000,10000), randint(-10000, 100000))
            e = randint(0,6)
            if e > 4:
                newfol = Foliage(cord, choice(["tree", "rock"]))
                self.foliage.append(newfol)

# Instantiation of stuff
main_cam = Camera(1)

playerGroup = pygame.sprite.Group()

player = Player((500,500), [25, 100, 2], "base", "right")
playerGroup.add(player)

fm = Folliage_Manager(multi=0.8)
fm.spawn_foliage(30000)

# settings

def get_dt():
    global last_time
    dt = 0
    time_now = time.time()
    dt = time_now - last_time
    last_time = time_now
    return dt * 300

left_border = -10000
right_border = 10000
top_border = -10000
bottom_border = 10000

def spawnBackground(res):
    global left_border, top_border, right_border, bottom_border

    for i in range(int(-1000*res/2),int(1000*res/2), 1000):
        for x in range(int(-1000*res/2),int(1000*res/2), 1000):
            screen.blit(images["background"], (main_cam.scroll[0]*-1 + i, main_cam.scroll[1]*-1 +x))
        #print(i)


def render(groups):
    spawnBackground(10)
    main_cam.follow(player, 60)

    for fol in fm.foliage:
        #if fol.rect.x > 0 and fol.rect.x < screen_width and fol.rect.y >0 and fol.rect.y > screen_height:
        if fol.on_screen():
            fol.update()
        #print("PRINTING R")

    for group in groups:
        group.update()
        group.draw(screen)

    screen.blit(images["enemies"]["simple"][0], (500-main_cam.scroll[0],300-main_cam.scroll[1]))

pygame.event.set_allowed([QUIT,KEYDOWN])

fpse = []

run = True
while run:
    clock.tick(fps)
    dt = int(get_dt())

    render([playerGroup])

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

        if event.type == KEYDOWN and event.key == K_ESCAPE:
            run = False

    fpse.append(int(clock.get_fps()))

    pygame.display.update()

print(f"""
MAX FPS:     {max(fpse)}
LEAST FPS:   {min(fpse)}
AVERAGE FPS: {int(sum(fpse)/len(fpse))}
""")

quit()
Asked By: RaptoRR

||

Answers:

The main issue is that your code is using time.time() which has a granularity of decimal seconds. It is multiplied by 300, but this looks like a fudge to speed it up??

It’s better to use the PyGame function pygame.time.get_ticks() for all in-game times. This function returns the number of milliseconds since your program started. So changing your get_dt():

def get_dt():
    global last_time
    dt = 0
    time_now = pygame.time.get_ticks()  # milliseconds since game start
    dt = time_now - last_time
    last_time = time_now
    #return dt * 300
    return dt

Makes the player move more quickly, and it fixes the occasional weird slowdowns I was seeing – maybe this is a rounding of the fractional seconds from time.time()? (I did not investigate why.)

You player animation uses simple "counter" based frame control. This means the speed of animation is directly tied to the on-screen frame-rate. It might work out better if this animation rate is tied to the PyGame clock.

The approach taken is to store the time of the next frame change. Then the code looks at the current time, comparing it with the future-time. If that time is now, the animation frame is changed, and the future-time recalculated. I also changed Player.count to Player.animating (boolean).

class Player(pygame.sprite.Sprite):
    def __init__(self, pos, stats, skin, ori):
        pygame.sprite.Sprite.__init__(self)

        # [...]
 
        self.ANIMATION_RATE = 50   # milliseconds between animation updates
        self.animating = False     # is the animation running (replaces self.count)
        # Future time of next animation frame
        self.next_anim_time = pygame.time.get_ticks() + self.ANIMATION_RATE

    def movement(self):
        self.keys = pygame.key.get_pressed()
        self.animating = False

        if self.keys[K_d]:
            self.x += self.speed * dt
            self.ori = "right"
            self.animating = True

        # [...]  Rest of identical changes omitted for the sake of brevity


    def animate(self):
        if ( self.animating ):
            time_now = pygame.time.get_ticks()
            # Is it time to do change the animation frame
            if ( time_now > self.next_anim_time ):
                self.next_anim_time = time_now + self.ANIMATION_RATE  # set time for next frame
                self.index += 1
                if ( self.index >= self.index_max ):
                    self.index = 0
                self.image = images["players"][self.skin+"_"+self.ori][self.index]

Note also that the animation function was changing the image every time (mostly back to the same thing), rather than when a change was actually needed.

Oh, and I never saw anything like a rock or leaf in testing. Maybe there’s a further issue here, or they’re meant to be rare items.

Answered By: Kingsley