Search code examples
pythonpygamesprite

Python pygame - Deleting offscreen sprites


I created a simple 2D game with python 2 and pygame where you have to avoid squares that are moving down. I created this class for the 'enemy':

class Enemy(pygame.sprite.Sprite):

  def __init__(self, game):
      self.groups = game.all_sprites
      pygame.sprite.Sprite.__init__(self, self.groups)
      self.game = game
      self.image = pygame.Surface((TILESIZE + 10, TILESIZE + 10))
      self.image.fill(ENEMY_COLOR)
      self.rect = self.image.get_rect()
      self.x = random.uniform(0, WIDTH - TILESIZE)
      self.rect.x = self.x
      self.y = 0

  def update(self):
      if self.rect.colliderect(self.game.player.rect):
          self.game.deaths += 1
          self.game.score = 0
          self.game.run()
      self.rect.y += (self.game.score + 500) / 50

Then, I have a thread that creates an instance of the enemy:

class Create_Enemy(Thread):

  def __init__(self):
      Thread.__init__(self)

  def run(self):
      while True:
          while not game.game_paused:
              time.sleep((game.score + 1) / game.speed)
              Enemy(game)

Then in the draw method i just write self.all_sprites.draw() The game is an infinite runner, and every frame the enemies move a few pixels down. The problem is that after a bit the game gets laggy since, when the blocks go offscreen, the sprites don't get deleted. Is there a way to automatically delete the offscreen instances?

I tried the following but it just deletes all the enemies onscreen.

if self.rect.y >= WIDTH:
    self.rect.kill()

I was thinking I could do something like game.all_sprites.pop(1) (the position 0 is the player) but I couldn't find a way to archive something similar ...


Solution

  • The enemies can remove themselfs from the game by calling self.kill() if their rect is no longer inside the screen, e.g.:

    def update(self):
        if self.rect.colliderect(self.game.player.rect):
            self.game.restart() # reset score + count deaths
    
        # we can simply use move_ip here to move the rect
        self.rect.move_ip(0, (self.game.score + 500) / 100)
    
        # check if we are outside the screen
        if not self.game.screen.get_rect().contains(self.rect):
            self.kill()
    

    Also, instead of the thread, you can use pygame's event system to spawn your enemies. Here's a simple, incomplete but runnable example (note the comments):

    import random
    
    import pygame
    
    TILESIZE = 32
    WIDTH, HEIGHT = 800, 600
    
    # a lot of colors a already defined in pygame
    ENEMY_COLOR = pygame.color.THECOLORS['red']
    PLAYER_COLOR = pygame.color.THECOLORS['yellow']
    
    # this is the event we'll use for spawning new enemies
    SPAWN = pygame.USEREVENT + 1
    
    class Enemy(pygame.sprite.Sprite):
    
        def __init__(self, game):
    
            # we can use multiple groups at once. 
            # for now, we actually don't need to
            # but we could do the collision handling with pygame.sprite.groupcollide()
            # to check collisions between the enemies and the playerg group
            pygame.sprite.Sprite.__init__(self, game.enemies, game.all)
            self.game = game
            self.image = pygame.Surface((TILESIZE + 10, TILESIZE + 10))
            self.image.fill(ENEMY_COLOR)
    
            # we can use named arguments to directly set some values of the rect
            self.rect = self.image.get_rect(x=random.uniform(0, WIDTH - TILESIZE))
            # we dont need self.x and self.y, since we have self.rect already
            # which is used by pygame to get the position of a sprite
    
    
        def update(self):
            if self.rect.colliderect(self.game.player.rect):
                self.game.restart() # reset score + count deaths
    
            # we can simply use move_ip here to move the rect
            self.rect.move_ip(0, (self.game.score + 500) / 100)
    
            # check if we are outside the screen
            if not self.game.screen.get_rect().contains(self.rect):
                self.kill()
    
    class Player(pygame.sprite.Sprite):
    
        def __init__(self, game):
            pygame.sprite.Sprite.__init__(self, game.all, game.playerg)
            self.game = game
            self.image = pygame.Surface((TILESIZE, TILESIZE))
            self.image.fill(PLAYER_COLOR)
            self.rect = self.image.get_rect(x=WIDTH/2 - TILESIZE/2, y=HEIGHT-TILESIZE*2)
    
        def update(self):
            # no nothing for now
            pass
    
    
    class Game(object):
    
        def __init__(self):
            # for now, we actually don't need mujtiple groups
            # but we could do the collision handling with pygame.sprite.groupcollide()
            # to check collisions between the enemies and the playerg group
            self.enemies = pygame.sprite.Group()
            self.all = pygame.sprite.Group()
            self.playerg = pygame.sprite.GroupSingle()
            self.running = True
            self.score = 0
            self.deaths = -1
            self.clock = pygame.time.Clock()
    
        def restart(self):
            # here we set the timer to create the SPAWN event
            # every 1000 - self.score * 2 milliseconds
            pygame.time.set_timer(SPAWN, 1000 - self.score * 2)
            self.score = 0
            self.deaths += 1
    
        def run(self):
            self.restart()
            self.player = Player(self)
            self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
    
            while self.running:
                for e in pygame.event.get():
                    if e.type == pygame.QUIT:
                        self.running = False
                    # when the SPAWN event is fired, we create a new enemy
                    if e.type == SPAWN:
                        Enemy(self)
    
                # draw and update everything
                self.screen.fill(pygame.color.THECOLORS['grey'])
                self.all.draw(self.screen)
                self.all.update()
                pygame.display.flip()
    
                self.clock.tick(40)
    
    if __name__ == '__main__':
        Game().run()