1
\$\begingroup\$

I've spent hours trying to figure why my collision detection was so crappy, with sprites visibly ovellaping for several frames, when I noticed the culprit could be pygame's detection callback pygame.sprite.collide_circle().

I've stripped down the code and created a simple demo, and results were surprising: collide_circle() still returns False for several frames after its sibling collide_rect() correctly detects a collision and returns True.

Here is the code:

import pygame

RED   = (255,   0,   0)
GREEN = (  0, 255,   0)
BLUE  = (  0,   0, 255)
BLACK = (  0,   0,   0)
WHITE = (255, 255, 255)

SCREEN_SIZE = (800, 600)
FPS = 60
BG_COLOR = BLUE

class Ball(pygame.sprite.Sprite):
    def __init__(self, color=WHITE, radius=10, position=[], velocity=[]):
        super(Ball, self).__init__()

        self.color = color
        self.radius = radius
        self.position = list(position) or [0, 0]
        self.velocity = list(velocity) or [0, 0]
        self.bounds = (screen.get_size()[0] - self.radius,
                       screen.get_size()[1] - self.radius)

        self.image = pygame.Surface(2*[self.radius*2])
        self.rect = self.image.get_rect()
        pygame.draw.circle(self.image, self.color, 2*(self.radius,), self.radius)

    def update(self, dt=1./FPS):
        super(Ball, self).update()

        for i in [0, 1]:
            self.position[i] += self.velocity[i] * dt

            if self.position[i] < self.radius:
                self.position[i] = self.radius
                self.velocity[i] *= -1

            elif self.position[i] > self.bounds[i]:
                self.position[i] = self.bounds[i]
                self.velocity[i] *= -1

        self.rect.center = self.position


pygame.init()

screen = pygame.display.set_mode(SCREEN_SIZE)
clock = pygame.time.Clock()

background = pygame.Surface(screen.get_size())
background.fill(BG_COLOR)
screen.blit(background, (0,0))

balls = pygame.sprite.Group()
balls.add(Ball(color=RED,   radius=100, position=[  0, 300], velocity=[ 100, 0]))
balls.add(Ball(color=GREEN, radius=100, position=[700, 300], velocity=[-100, 0]))

done = False
while not done:
    for event in pygame.event.get():
        if (event.type == pygame.QUIT or
            event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
            done = True

    balls.update()
    if pygame.sprite.collide_rect(*balls):
        collision = pygame.sprite.collide_circle(*balls)
        print collision
        if collision:
            for ball in balls:
                ball.velocity[0] *= -1

    balls.clear(screen, background)
    balls.draw(screen)
    pygame.display.update()
    clock.tick(FPS)

pygame.quit()

And the output:

False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
True
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False

That's 17 frames of overlap before collide_circle() realizes that hey, there's a collision there! Even at 60 FPS that's huge.

The balls are almost halfway through each other, and this behavior seems to be velocity independent: if I change the balls' speed or the FPS, the visual result (halfway overlap) remains, but for fewer frames.

So, my question is: is this a pygame bug? Or did I completely misunderstood how detection works?

\$\endgroup\$
  • \$\begingroup\$ Not a complete answer, but did you consider doing an AABB/rectangle collision check first and then using collide_circle if there's a positive result? \$\endgroup\$ – ashes999 Aug 5 '14 at 21:11
  • \$\begingroup\$ @ashes999 That was my initial idea, but the problem is in this demo the circles are colliding head-on horizontally, so results from collide_rect() and collide_circle() should be the same. All those False are not false positives, and collide_circle() should be able to detect them. \$\endgroup\$ – MestreLion Aug 6 '14 at 7:28
  • \$\begingroup\$ @ashes999 also, note that I am using rectangle collision first! I'm only printing the cases that pass the test if pygame.sprite.collide_rect(*balls): \$\endgroup\$ – MestreLion Aug 6 '14 at 7:32
  • \$\begingroup\$ did you test if collision is true by finding the distance between the circles and if the sum of there radii is more than the distance, they are colliding? \$\endgroup\$ – Serial Aug 13 '14 at 22:24
  • \$\begingroup\$ @Serial yes, that's what I ended up doing myself, and it worked great, but isn't that what pygame.sprite.collide_circle() is supposed to do? \$\endgroup\$ – MestreLion Aug 14 '14 at 0:24
1
\$\begingroup\$

Which version of PyGame were you using? An issue seems to have been fixed at some point between 1.9.1 and 1.9.2a concerning collide_circle() (see here for fix diff).

It might be worth it to try 1.9.2a if possible (download/source here), which I believe isn't actually up on pygame.org. If you're on Linux or OS X you'll have to build 1.9.2a from source.

\$\endgroup\$
  • \$\begingroup\$ I'm using 1.9.1release+dfsg-5, the one that ships with Ubuntu 12.04. It's weird that 1.9.2 isn't packaged yet in Debian/Ubuntu, considering it was released in 2012. And thanks a lot for the links! \$\endgroup\$ – MestreLion Nov 21 '14 at 5:40
  • \$\begingroup\$ I've just found a Debian bug report with the same problem, and the suggested fix is included in the diff you posted, so it seems that issue was indeed fixed in 1.9.2. bugs.debian.org/cgi-bin/bugreport.cgi?bug=681975 \$\endgroup\$ – MestreLion Nov 21 '14 at 5:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.