1

I'm making a game where you throw snowballs at snowmen. Here are the definitions for the Snowman class and Snowball class:

class Snowman:
    def __init__(self,start,end,a,b):
        self.x = random.randrange(0,310)
        self.y = 0
        self.r = pygame.Rect(self.x,self.y,a,b)

class Snowball:
    def __init__(self,a,b):
        self.x = mousex
        self.y = mousey
        self.r = pygame.Rect(self.x,self.y,a,b)

As you can see, the r attribute for the classes is their rectangle.

Below is a script that controls the snowman AI.

for s in snowmen:
    s.y += 0.5
    #above_makes_the_snowmen_approach_the_bottom_of_the_screen.
    #below_is_supposed_to_check_for_snowball_collision.
    for b in bullets:
        if s.r.colliderect(b.r):
            snowmen.remove(s)
            bullets.remove(b)
        print(b.r.colliderect(s.r))
        #above_is_a_debugger_that_reports_the_collision.
    if s.y < 640:
        blit(asnowman,(50,78),(s.x,s.y))
    else:
        snowmen.remove(s)

The problem is when I throw a snowball at one of the snowmen, the debugger reports False and the snowman doesn't die. How do I fix this?

martineau
  • 119,623
  • 25
  • 170
  • 301
Oxnvat
  • 49
  • 4
  • Your code is (potentially) removing items in the two lists *while* it is iterating over them, which is always error prone. – martineau Jan 30 '22 at 21:27
  • The script is supposed to destroy both the snowman and the snowball upon impact – Oxnvat Jan 30 '22 at 21:29
  • I understand that. The problem is that it does so inside a `for` loop that is iterating over the list. See this [answer of mine](https://stackoverflow.com/questions/4081217/how-to-modify-list-entries-during-for-loop/4082739#4082739) on the topic (and how to work around it). – martineau Jan 30 '22 at 21:33

2 Answers2

2

See How do I detect collision in pygame?. You need to update the position of the rectangles before the collision test:

s.r.topleft = round(s.x), round(s.y)
b.r.topleft = round(b.x), round(b.y)
if s.r.colliderect(b.r):

Note, you cannot get rid of the x and y attribute, because pygame.Rect is supposed to represent an area on the screen and a pygame.Rect object can only store integral data.

The coordinates for Rect objects are all integers. [...]


Also read How to remove items from a list while iterating?. A possible solution could be to iterate through copies of the list:

for s in snowmen[:]:
    s.y += 0.5
    s.r.topleft = round(s.x), round(s.y)
    for b in bullets[:]:
        b.r.topleft = round(b.x), round(b.y)
        if s.r.colliderect(b.r):
            snowmen.remove(s)
            bullets.remove(b)
            break
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
2

While you initialize the Rect object in each of your classes to match the x and y attributes when the object is created, they do not remain in sync. That means that you only ever collide the rectangles at their initial positions, not where the objects have moved to later on.

The easiest way to solve this is probably to add a few lines of code to update s.r and/or b.r, but you could also start using pygame.sprite.Sprite, which will update its rect attribute to match its x and y attributes (and vice versa, I think). If you wanted to make your own classes behave a little bit like a Sprite without going all the way, I'd suggest making x and y into property descriptors, which reference the Rect, which becomes the single source of truth:

class Snowman:
    def __init__(self,start,end,a,b):
        self.r = pygame.Rect(random.randrange(0,310),0,a,b)

    @property
    def x(self):
        return self.r.left
    @x.setter
    def x(self, value):
        self.r.left = value

    @property
    def y(self):
        return self.r.top
    @y.setter
    def y(self, value):
        self.r.top = value
Blckknght
  • 100,903
  • 11
  • 120
  • 169