pygame Get the balls to bounce off each other

I'm trying to get the balls to bounce off each other. I tried to do it using the reflect() method, but it doesn't work for some reason. To detect the balls, I used groupcollide, since I couldn't think of a better way, maybe this is wrong?

import pygame
import random

class Ball(pygame.sprite.Sprite):

    def __init__(self, startpos, velocity, startdir):
        super().__init__()
        self.pos = pygame.math.Vector2(startpos)
        self.velocity = velocity
        self.dir = pygame.math.Vector2(startdir).normalize()
        self.image = pygame.image.load("small_ball.png").convert_alpha()
        self.rect = self.image.get_rect(center = (round(self.pos.x), round(self.pos.y)))

    def reflect(self, NV):
        self.dir = self.dir.reflect(pygame.math.Vector2(NV))

    def update(self):
        self.pos += self.dir * 10
        self.rect.center = round(self.pos.x), round(self.pos.y)

        if self.rect.left <= 0:
            self.reflect((1, 0))
        if self.rect.right >= 700:
            self.reflect((-1, 0))
        if self.rect.top <= 0:
            self.reflect((0, 1))
        if self.rect.bottom >= 700:
            self.reflect((0, -1))

pygame.init()
window = pygame.display.set_mode((700, 700))
pygame.display.set_caption('noname')
clock = pygame.time.Clock()

all_balls = pygame.sprite.Group()

start, velocity, direction = (200, 200), 10, (random.random(), random.random())
ball_1 = Ball(start, velocity, direction)

start, velocity, direction = (200, 200), 10, (random.random(), random.random())
ball_2 = Ball(start, velocity, direction)

all_balls.add(ball_1, ball_2)

run = True
while run:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    all_balls.update()

    hits = pygame.sprite.groupcollide(all_balls, all_balls, False, False)
    for _, hit_sprites in hits.items():
        if len(hit_sprites) > 1:
            sprite_1 = hit_sprites[0]
            sprite_2 = hit_sprites[1]
            sprite_1.reflect((0, 1)) # not working
            print("hit!")

    window.fill(0)
    pygame.draw.rect(window, (255, 0, 0), (0, 0, 700, 700), 1)
    all_balls.draw(window)
    pygame.display.flip()

small_ball.png


Solution 1:

Define a circle by the ball Sprites and use the algorithm from the answer to Pygame how to let balls collide for a function, that can compute reflection of bouncing balls:

def reflectBalls(ball_1, ball_2):
    v1 = pygame.math.Vector2(ball_1.rect.center)
    v2 = pygame.math.Vector2(ball_2.rect.center)
    r1 = ball_1.rect.width // 2
    r2 = ball_2.rect.width // 2
    if v1.distance_to(v2) < r1 + r2 - 2:
        nv = v2 - v1
        if nv.length() > 0:
            ball_1.dir = ball_1.dir.reflect(nv)
            ball_2.dir = ball_2.dir.reflect(nv)

Ensure that the balls have different initial positions:

start, velocity, direction = (200, 200), 10, (random.random(), random.random())
ball_1 = Ball(start, velocity, direction)

start, velocity, direction = (300, 300), 10, (random.random(), random.random())
ball_2 = Ball(start, velocity, direction)

Alternatively you can avoid that the balls are sticking together by improving the bounce algorithm. Only bounce the balls, if the next positions of the balls are closer then the current positions:

def reflectBalls(ball_1, ball_2):
    v1 = pygame.math.Vector2(ball_1.rect.center)
    v2 = pygame.math.Vector2(ball_2.rect.center)
    r1 = ball_1.rect.width // 2
    r2 = ball_2.rect.width // 2
    d = v1.distance_to(v2)
    if d < r1 + r2 - 2:
        dnext = (v1 + ball_1.dir).distance_to(v2 + ball_2.dir)
        nv = v2 - v1
        if dnext < d and nv.length() > 0:
            ball_1.dir = ball_1.dir.reflect(nv)
            ball_2.dir = ball_2.dir.reflect(nv)

Test if each ball collides with any other ball:

ball_list = all_balls.sprites()
for i, b1 in enumerate(ball_list):
    for b2 in ball_list[i+1:]:
        reflectBalls(b1, b2)

Complete example: repl.it/@Rabbid76/PyGame-BallsBounceOff

import pygame
import random

class Ball(pygame.sprite.Sprite):

    def __init__(self, startpos, velocity, startdir):
        super().__init__()
        self.pos = pygame.math.Vector2(startpos)
        self.velocity = velocity
        self.dir = pygame.math.Vector2(startdir).normalize()
        self.image = pygame.image.load("small_ball.png").convert_alpha()
        self.rect = self.image.get_rect(center = (round(self.pos.x), round(self.pos.y)))

    def reflect(self, NV):
        self.dir = self.dir.reflect(pygame.math.Vector2(NV))

    def update(self):
        self.pos += self.dir * 10
        self.rect.center = round(self.pos.x), round(self.pos.y)

        if self.rect.left <= 0:
            self.reflect((1, 0))
            self.rect.left = 0
        if self.rect.right >= 700:
            self.reflect((-1, 0))
            self.rect.right = 700
        if self.rect.top <= 0:
            self.reflect((0, 1))
            self.rect.top = 0
        if self.rect.bottom >= 700:
            self.reflect((0, -1))
            self.rect.bottom = 700

pygame.init()
window = pygame.display.set_mode((700, 700))
pygame.display.set_caption('noname')
clock = pygame.time.Clock()

all_balls = pygame.sprite.Group()

start, velocity, direction = (200, 200), 10, (random.random(), random.random())
ball_1 = Ball(start, velocity, direction)

start, velocity, direction = (300, 300), 10, (random.random(), random.random())
ball_2 = Ball(start, velocity, direction)

all_balls.add(ball_1, ball_2)

def reflectBalls(ball_1, ball_2):
    v1 = pygame.math.Vector2(ball_1.rect.center)
    v2 = pygame.math.Vector2(ball_2.rect.center)
    r1 = ball_1.rect.width // 2
    r2 = ball_2.rect.width // 2
    d = v1.distance_to(v2)
    if d < r1 + r2 - 2:
        dnext = (v1 + ball_1.dir).distance_to(v2 + ball_2.dir)
        nv = v2 - v1
        if dnext < d and nv.length() > 0:
            ball_1.reflect(nv)
            ball_2.reflect(nv)

run = True
while run:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    all_balls.update()

    ball_list = all_balls.sprites()
    for i, b1 in enumerate(ball_list):
        for b2 in ball_list[i+1:]:
            reflectBalls(b1, b2)

    window.fill(0)
    pygame.draw.rect(window, (255, 0, 0), (0, 0, 700, 700), 1)
    all_balls.draw(window)
    pygame.display.flip()