Extremely poor performance in my simulation made in PyGame

158 Views Asked by At

I followed a PyGame tile-based tutorial for a project in school, but I never intended to make a game, but a simulation of an ecosystem. Unfortunately, when I run my program the performance is very bad and it only manages to run for a few seconds, before the windows to stop answering.

The only thing I want to do at the moment is to place a new patch of grass, when the energy of a grass patch reaches 80.

What is there to do? Is it bad that everything is inside of the update method? Can I use events or something to make the checks happen with a greater interval? I know there is a lot of maths going on each frame, but don't know how to do it another way.

Here is my code:

main.py:

#!/usr/bin/python3

#Importing necessary libraries
import pygame as pg, sys, random
from settings import *
from sprites import *

class Sim:
    #Initialize the game window, etc.
    def __init__(self):
        pg.init()
        self.screen = pg.display.set_mode((WIDTH, HEIGHT))
        pg.display.set_caption(TITLE)
        self.clock = pg.time.Clock()

        self.running = True

    def new_grass(self, pos):
        for g in self.grass:
            if pos != g.pos:
                Grass(self, pos)

    #Start a new generation
    def new(self):
        self.all_sprites = pg.sprite.Group()
        self.grass = pg.sprite.Group()
        Grass(self, (10, 15))
        self.run()

    #Main loop
    def run(self):
        self.simulating = True
        while self.simulating:
            self.clock.tick(FPS)
            self.events()
            self.update()
            self.draw()

    #Update things on screen
    def update(self):
        self.all_sprites.update()

    #Draw a grid on screen
    def draw_grid(self):
        for x in range(0, WIDTH, TILESIZE):
            pg.draw.line(self.screen, BLACK, (x, 0), (x, HEIGHT))
        for y in range(0, HEIGHT, TILESIZE):
            pg.draw.line(self.screen, BLACK, (0, y), (WIDTH, y))

    #Draw things on screen
    def draw(self):
        pg.display.set_caption("{:.2f}".format(self.clock.get_fps()))
        self.screen.fill(DARK_GREEN)
        self.draw_grid()
        self.all_sprites.draw(self.screen)
        #After drawing everything, flip the display
        pg.display.flip()

    #Events that might happen
    def events(self):
        for event in pg.event.get():
            #Check for the user closing the window
            if event.type == pg.QUIT:
                if self.simulating:
                    self.simulating = False
                self.running = False

s = Sim()
while s.running:
    s.new()
pg.quit()

sprites.py:

#!/usr/bin/python3

import pygame as pg, random
from settings import *
vec = pg.math.Vector2

class Grass(pg.sprite.Sprite):
    def __init__(self, sim, cord):
        self.groups = sim.all_sprites, sim.grass
        pg.sprite.Sprite.__init__(self, self.groups)
        self.sim = sim
        self.image = pg.Surface((TILESIZE/2, TILESIZE/2))
        self.image.fill(GREEN)
        self.cord = cord
        self.rect = self.image.get_rect()
        self.pos = vec(cord) * TILESIZE / 2
        self.rect.topleft = self.pos
        self.spread = vec(random.randint(-1, 1), random.randint(-1, 1))
        self.energy = 20

    def update(self):
        if self.energy <= 80:
            self.energy += 10

        if self.energy >= 80:
            self.sim.new_grass((self.cord + self.spread))

settings.py:

#Options/settings
TITLE = "EcoSim"
WIDTH = 480
HEIGHT = 600
FPS = 30
TILESIZE = 32
GRID_WIDTH = WIDTH / TILESIZE
GRID_HEIGHT = HEIGHT / TILESIZE

#Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
DARK_GREEN = (0, 100, 0)
BROWN = (150,75,0)
2

There are 2 best solutions below

4
On BEST ANSWER

The problem is here:

def new_grass(self, pos):
    for g in self.grass:
        if pos != g.pos:
            Grass(self, pos)

This is because you'll add a Grass object for every time there is a grass that already exists on another position. I think you meant to add one if the position isn't present at all.

You have a few bugs in your program, mainly the one mentioned above, but also, the parameter pos should actually be coord. I've highlighted your code with some comments on improvements:

#!/usr/bin/python3

import pygame as pg, random

TITLE = "EcoSim"
WIDTH = 480
HEIGHT = 600
FPS = 30
TILESIZE = 32
GRID_WIDTH = WIDTH / TILESIZE
GRID_HEIGHT = HEIGHT / TILESIZE

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
DARK_GREEN = (0, 100, 0)
BROWN = (150,75,0)

vec = pg.math.Vector2

class Grass(pg.sprite.Sprite):
    # This creates the image just once!
    IMAGE = pg.Surface((TILESIZE/2, TILESIZE/2))
    IMAGE.fill(GREEN)

    def __init__(self, cord):  # Remove 'sim'.
        # self.groups = sim.all_sprites, sim.grass
        # pg.sprite.Sprite.__init__(self, self.groups)
        # self.sim = sim
        super().__init__()
        self.image = Grass.IMAGE  # All reference the same image.
        self.cord = cord
        self.rect = self.image.get_rect()
        self.pos = vec(cord) * TILESIZE / 2
        self.rect.topleft = self.pos
        self.energy = 20
        self.spread = vec(random.randint(-1, 1), random.randint(-1, 1))

    def update(self):
        if self.energy <= 80:
            self.energy += 10

        # Make Sim check for this.
        # if self.energy >= 80:
        #     self.sim.new_grass((self.cord + self.spread))


class Sim:
    def __init__(self):
        pg.init()
        pg.display.set_caption(TITLE)
        self.screen = pg.display.set_mode((WIDTH, HEIGHT))
        self.clock = pg.time.Clock()
        self.all_sprites = pg.sprite.Group()  # Create *ALL* attributes in `__init__`
        self.grass = pg.sprite.Group()
        self.running = True
        self.simulating = False

    # You're passing coord here, not pos! And you also want to add
    # the grass only if the coord is not already present in the list.
    def new_grass(self, coord):
        if coord not in (x.cord for x in self.grass):
            grass = Grass(coord)
            self.grass.add(grass)
            self.all_sprites.add(grass)

    def new(self):
        self.all_sprites = pg.sprite.Group()
        self.grass = pg.sprite.Group()
        grass = Grass((10, 15))  # Grass is now pure and doesn't have any side-effects, which makes the code much clearer.
        self.grass.add(grass)
        self.all_sprites.add(grass)
        self.run()

    def run(self):
        self.simulating = True
        while self.simulating:
            self.clock.tick(FPS)
            self.events()
            self.update()
            self.draw()

    def update(self):
        self.all_sprites.update()
        # Let Sim decide the fate of the grass. Don't let Grass add
        # itself.
        for grass in self.grass:
            if grass.energy >= 80:
                self.new_grass((grass.cord + grass.spread))

    def draw_grid(self):
        for x in range(0, WIDTH, TILESIZE):
            pg.draw.line(self.screen, BLACK, (x, 0), (x, HEIGHT))
        for y in range(0, HEIGHT, TILESIZE):
            pg.draw.line(self.screen, BLACK, (0, y), (WIDTH, y))

    def draw(self):
        pg.display.set_caption("{:.2f}".format(self.clock.get_fps()))
        self.screen.fill(DARK_GREEN)
        self.draw_grid()
        self.all_sprites.draw(self.screen)
        pg.display.flip()

    def events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.simulating = False  # There was an unnecessary if here.
                self.running = False

s = Sim()
while s.running:
    s.new()
pg.quit()
0
On

The major issue is the method Sim.new_grass:

class Sim:
   # [...]

   def new_grass(self, pos):
       for g in self.grass:
           if pos != g.pos:
               Grass(self, pos)

The method generates many more Grass objects than expected. It event generates multiple grass objects at the same location. For every object (g) where pos != g.pos a new instance of Grass is constructed.

You need to create a new instance of Grass if there is not any Grass object in the desired location:

class Sim:
   # [...]

   def new_grass(self, pos):
        if not any(pos == g.pos for g in self.grass):
            Grass(self, pos)