Search code examples
pygamecollision-detectionpython-3.4rectsurface

colliderect() triggers unexpectedly


I am making a classic Snake game. I want to draw a green rectangle (green_rect), and whenever my snake touches it, that green rectangle moves to a new, random location. When I run colliderect() on the Worm's surface, it always returns True for some reason, though, even if the worm doesn't touch the rectangle.

So far I've tried:

width  = 640 
height = 400
screen = pygame.display.set_mode((width, height))
green  = pygame.Surface((10,10)) #it's the rectangle
green.fill((0,255,0))
green_rect = pygame.Rect((100,100), green.get_size())

  # Our worm.
w = Worm(screen, width/2, height/2, 100)   # I have a class called Worm, described later...

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

        elif event.type == pygame.KEYDOWN:
            w.key_event(event)   # direction control method in the Worm class

    if green_rect.colliderect(w.rect):  # w.rect is coming from Worm class: self.rect=self.surface.get_rect()
        # surface variable is 100 coming from: w = Worm(screen, width/2, height/2, 100) -- it describes the worm's length in segments / pixels.
        green_rect.x, green_rect.y = (random.randrange(420),random.randrange(300))


    screen.fill((0, 0, 0))
    screen.blit(green, green_rect)

    w.move() #function for movement in Worm class
    w.draw() #function for drawing the snake in Worm class

But this doesn't work, the green rectangle is moving uncontrollably, even if I don't touch it with the snake. colliderect must not be working since the x-y coordinates of the green rectangle are changing without the snake actually touching it. It should only change location when the snake touches the green rectangle.

I didn't show all of my code, because its little long. If it necessary I can write out Class Worm as well. Well rect method is not working on lists, so I couldn't figure how to fix this problem.

Edit: Also I want to know how to resize the snake's segments, since my default size is a pixel (dot). I want it bigger so if it hits a big rectangle, even on a corner of the rectangle or along a side, rectangle will still move.

Edit-2: Here is my Worm class:

class Worm():
    def __init__(self, surface, x, y, length):
        self.surface = surface
        self.x = x
        self.y = y
        self.length  = length
        self.dir_x   = 0
        self.dir_y   = -1
        self.body    = []
        self.crashed = False


    def key_event(self, event):
        """ Handle key events that affect the worm. """
        if event.key == pygame.K_UP:
            self.dir_x = 0
            self.dir_y = -1
        elif event.key == pygame.K_DOWN:
            self.dir_x = 0
            self.dir_y = 1
        elif event.key == pygame.K_LEFT:
            self.dir_x = -1
            self.dir_y = 0
        elif event.key == pygame.K_RIGHT:
            self.dir_x = 1
            self.dir_y = 0


    def move(self):
        """ Move the worm. """
        self.x += self.dir_x
        self.y += self.dir_y

        if (self.x, self.y) in self.body:
            self.crashed = True

        self.body.insert(0, (self.x, self.y))

        if len(self.body) > self.length:
            self.body.pop()


    def draw(self):
        for x, y in self.body:
            self.surface.set_at((int(x),int(y)), (255, 255, 255))

Solution

  • I think I understand what's going on here: Your class Worm consists of an object that contains a list of xy-tuples, one for each pixel it occupies. The 'worm' creeps across the playfield, adding the next pixel in its path to that list and discarding excess pixel locations when it's reached its maximum length. But Worm never defines its own rect and Worm.surface is actually the surface of the display area.

    The reason why your target green_rect hops around all the time is because w.surface refers to screen, not the Worm's own pixel area. w.surface.get_rect() is therefore returning a rectangle for the entire display area, as suspected. The green rectangle has nowhere to go that doesn't collide with that rect.

    With that in mind, here's a solution for you:

      # in class Worm...
    def __init__(self, surface, x, y, length):
        self.surface = surface
        self.x = x
        self.y = y
        self.length = length
        self.dir_x = 0
        self.dir_y = -1
        self.body = []
        self.crashed = False
    
        self.rect = pygame.Rect(x,y,1,1)  # the 1,1 in there is the size of each worm segment!
          # ^ Gives the 'head' of the worm a rect to collide into things with.
    
      #...
    
    def move(self):
        """ Move the worm. """
        self.x += self.dir_x
        self.y += self.dir_y
    
        if (self.x, self.y) in self.body:
            self.crashed = True
    
        self.body.insert(0, (self.x, self.y))
    
        if len(self.body) > self.length:
            self.body.pop()
    
        self.rect.topleft = self.x, self.y
          # ^ Add this line to move the worm's 'head' to the newest pixel.
    

    Remember to import pygame, random, sys at the top of your code and throw in pygame.display.flip() (or update, if you have a list of updating rects) at the end and you should be set.

    As it stands, your code for a worm that crashes into itself runs, but has no effect.

    One more suggestion: you may want to rename Worm.surface to something like Worm.display_surface, since pygame has a thing called Surface as well, and it might make it a bit easier for others to understand if the two were more clearly defined.


    If you're looking for a more sophisticated worm consisting of a list of Rects, that's also possible. Just replace the xy-tuples in the Worm.body list with rects and replace if (self.x, self.y) in self.body: with if self.rect.collidelist(self.body) != -1: and it ought to work the same way. (collidelist(<list of Rects>) returns the index of the first colliding rect in that list, or -1 if there are none.)

    Just make sure you also change the self.rect in class Worm so that it's more like

    `self.rect = pygame.Rect(x,y, segment_width, segment_height)`
    

    ...and adjust self.dir_x and self.dir_y so that they're equal to +/- segment_width and segment_height, respectively. You'll need to fill each segment or use pygame.draw.rects instead of using set_at in Worm.draw(), as well.


    UPDATE: Variable-size worm segments

    Here's some code that uses Rects for the worm's body instead of xy-tuples. I have it set up so that the worm will define the size of each segment during initialization, then move forward in steps of that size (depending the axis of travel). Be aware the worm moves very quickly, has nothing stopping it from escaping the play area, and is quite long at 100 units length!

    Here's my solution, in any case-- First is the revised class Worm code. I've marked altered lines with comments, including the lines that are new as of the original answer.

    class Worm(pygame.sprite.Sprite): # MODIFIED CONTENTS!
        def __init__(self, surface, x, y, length, (seg_width, seg_height)): # REVISED
            self.surface = surface
            self.x = x
            self.y = y
            self.length = length
            self.dir_x = 0
            self.dir_y = -seg_width # REVISED
            self.body = []
            self.crashed = False
    
            self.rect = pygame.Rect(x,y,seg_width,seg_height) # NEW/REVISED
            self.segment = pygame.Rect(0,0,seg_width,seg_height) # NEW
    
    
        def key_event(self, event):
            """ Handle key events that affect the worm. """
            if event.key == pygame.K_UP:
                self.dir_x = 0
                self.dir_y = -self.segment.height # REVISED
            elif event.key == pygame.K_DOWN:
                self.dir_x = 0
                self.dir_y = self.segment.height # REVISED
            elif event.key == pygame.K_LEFT:
                self.dir_x = -self.segment.width # REVISED
                self.dir_y = 0
            elif event.key == pygame.K_RIGHT:
                self.dir_x = self.segment.width # REVISED
                self.dir_y = 0
    
    
        def move(self):
            """ Move the worm. """
            self.x += self.dir_x
            self.y += self.dir_y
    
            self.rect.topleft = self.x, self.y # NEW/MOVED
    
            if self.rect.collidelist(self.body) != -1: # REVISED
                self.crashed = True
                print "CRASHED!"
    
            new_segment = self.segment.copy() # NEW
            self.body.insert(0, new_segment.move(self.x, self.y)) # REVISED
    
            if len(self.body) > self.length:
                self.body.pop()
    
    
        def draw(self): # REVISED
            for SEGMENT in self.body:   # REPLACEMENT
                self.surface.fill((255,255,255), SEGMENT) # REPLACEMENT
    

    ...you'll have to revise the arguments when you assign w, too, to include your desired worm segment dimensions. In this case, I've closen 4 pixels wide by 4 pixels tall.

    w = Worm(screen, width/2, height/2, 100, (4,4)) # REVISED
    

    That's what I'd do. Note that you can assign whatever segment size you like (as long as each dimension is an integer greater than zero), even using single-pixel or rectangular (which is, 'non-square') segments if you like.