Search code examples
pythonpygamecollision-detectionspriterect

Collision not working for pygame Sprites


I'm trying to make a spaceship game for my Python programming class, and I want to use sprite.spritecollideany to check if my player sprite spaceship is colliding with any of the asteroid sprites in the spriteGroup group, but no matter what I do, it doesn't seem to work.

Here's the code:

import pygame
import random
pygame.init()
screen = pygame.display.set_mode((600, 600))
pygame.display.set_caption("Asteroids and Spaceships")

background = pygame.image.load("background.png")
background = background.convert()

white = 255,255,255

#first I make the asteroids with this class.
class asteroids(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.x = x
        self.y = y
        self.width = 30
        self.height = 30

        self.i1 = pygame.image.load("smallasteroid.png")
        self.i1 = self.i1.convert()
        self.i1.set_colorkey(white)
        self.rect = self.i1.get_rect()
        self.rect.left = x
        self.rect.top = y

        self.i2 = pygame.image.load("smallasteroid2.png")
        self.i2 = self.i2.convert()
        self.i2.set_colorkey(white)
        self.rect = self.i2.get_rect()
        self.rect.left = x
        self.rect.top = y

        self.i3 = pygame.image.load("mediumasteroid.png")
        self.i3 = self.i3.convert()
        self.i3.set_colorkey(white)
        self.rect = self.i3.get_rect()
        self.rect.left = x
        self.rect.top = y

        self.current = 0

    def render(self, image_num):
        if image_num == 1:
            self.current = 1
        if image_num == 2:
            self.current = 2
        if image_num == 3:
            self.current = 3

    def update(self):
        if self.current == 1:
            screen.blit(self.i1, (self.x,self.y))
            self.y += random.randint(7,11)
            if self.y > screen.get_height():
                self.y = 0

        if self.current == 2:
            screen.blit(self.i2, (self.x,self.y))
            self.y += random.randint(5,9)
            if self.y > screen.get_height():
                self.y = 0

        if self.current == 3:
            screen.blit(self.i3, (self.x,self.y))
            self.y += random.randint(3,6)
            if self.y > screen.get_height():
                self.y = 0

#and then this is the class for the spaceship   
class spaceship(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.x = x
        self.y = y
        self.width = 40
        self.height = 60
        self.image = pygame.image.load("spaceship.png")
        self.image = self.image.convert()
        self.image.set_colorkey(white)
        self.rect = self.image.get_rect()
        self.rect.left = x
        self.rect.top = y

    def update(self):
        screen.blit(self.image,(self.x,self.y))
        if self.y > screen.get_height():
            self.y = 0
        if self.y < screen.get_height()-610:
            self.y = screen.get_height()
        if self.x > screen.get_width():
            self.x = 0
        if self.x < screen.get_width()-610:
            self.x = screen.get_width()

#main is where I have the game run
def main():
    x_ship, y_ship = 0,0
    player = spaceship(300,550)

    spriteGroup = pygame.sprite.Group() #my asteroid sprites are grouped here

    for i in range(25):
        image_num = random.randint(0,3)
        asteroid = asteroids(random.randint(0,500),random.randint(0, 200))
        asteroid.render(image_num)
        spriteGroup.add(asteroid)

    clock = pygame.time.Clock()
    keepGoing = True


    while keepGoing:
        #this is what I tried to use to check for collisions. I know it's not working but I don't know why.
        if pygame.sprite.spritecollideany(player,spriteGroup):
            #the program quits on collision.
            pygame.quit()
        for event in pygame.event.get():
            if (event.type == pygame.QUIT):
                keepGoing = False
            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_ESCAPE:
                    keepGoing = False

            if (event.type == pygame.KEYDOWN):
                if (event.key == pygame.K_LEFT):
                    x_ship = -4
                if (event.key == pygame.K_RIGHT):
                    x_ship = 4
                if (event.key == pygame.K_UP):
                    y_ship = -4
                if (event.key == pygame.K_DOWN):
                    y_ship = 4

            if (event.type==pygame.KEYUP):
                if (event.key==pygame.K_LEFT):
                    x_ship = 0
                if (event.key==pygame.K_RIGHT):
                    x_ship = 0 
                if (event.key==pygame.K_UP):
                    y_ship = 0
                if (event.key==pygame.K_DOWN):
                    y_ship = 0

        player.x += x_ship
        player.y += y_ship
        screen.blit(background, (0,0))
        spriteGroup.clear(screen, background)
        player.update()
        spriteGroup.update()
        clock.tick(50)
        pygame.display.flip()

    pygame.quit()

main()

I can't figure out why the collision isn't working.


Solution

  • Your problem is that neither the spaceship nor asteroids classes revise their own rects (their hitboxes) on update, and their x and y attributes have no direct or automatic connection to the location of that rect. If you add something like self.rect.topleft = self.x, self.y to the end of your update function for both classes, their respective rects-- which is, your hitboxes-- will move to where they ought to be, instead of remaining at their initialized locations, which in this case is (300,550) for player and... some semi-random offset for each asteroid (I'm not sure where, exactly; all I did was make a sloppy reproduction of your code, then test a bunch of hunches. I apologize for not finding the exact origin of the problem...)

    In any case, the short answer is that, although you have a running check for the x and y locations of each sprite, you haven't told pygame to actually apply that location to the hitbox, and sprite.spritecollideany is always Falsey because the hitbox rects themselves were never actually touching.

    Putting self.rect.topleft = self.x, self.y at the end of each of your sprite class' update functions will fix it. (Make sure that this line is at the end of the function and at the lowest indentation level within def update(self)!)

    EDIT

    Alternatively, instead of adding the aforementioned code to update, you could replace player.x = x_ship and player.y = y_ship in your main loop with something like:

    while keepGoing:
       ...
        player.rect.move_ip(x_ship, y_ship)  # move the ship's rect
        player.x, player.y = player.rect.topleft  # update it's x and y, if you use these elsewhere
    
        for item in spriteGroup: # update the rects for your asteroid objects
            item.rect.topleft = item.x, item.y
    

    I would use the update-altering solution, since there's a really good chance that this solution will cause you grief when the player approaches the edges of the play field. Still, another avenue for you to consider.

    As a suggestion, you could redefine <Sprite>.x and <Sprite>.y as propertys that return self.rect.left and return self.rect.top, respectively, so that the x and y values are pinned to the topleft of your hitbox. A setter, too, even.

    I'm not sure how you may wish to use those parameters in the future; it's probably possible for you to eliminate them entirely, if you like, and use their rects' locators instead. Food for thought!

    A note:

    I'm also assuming that all of your sprite's x and y attributes refer to the top left point of its rect box. If that point is supposed to be somewhere else (the center, for instance), then you may need to make adjustments to this code, if you decide to use it.