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))
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 Rect
s, that's also possible. Just replace the xy-tuples in the Worm.body
list with rect
s 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 Rect
s 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.