okayatcp12
okayatcp12

Reputation: 318

Pygame Ball Collision

I am trying to simulate an elastic collision in pygame between two balls. The problem is, the balls sometimes don't follow the proper behaviour and sometimes "vibrate" or "stick" to each other or they even just go through each other when a collision happens. I don't know whether this has something to do with the equation I used (should be pretty standard but I really don't know). Here is my code:

import pygame
import random
import math

def checkcirclecollide(x1, y1, r1, x2, y2, r2):
    return (x1 - x2)**2 + (y1 - y2)**2 == (r1 + r2)**2

def ballcollision(m1, m2, v1, v2):
    v2f = (2*m1*v1+m2*v2-m1*v2)/(m1+m2)
    v1f = (m1*v1+m2*v2-m2*v2f)/m1
    return v1f, v2f

class Ball:
    def __init__(self, x, y, vx, vy, r, m):
        self.x = x
        self.y = y
        self.vx = vx
        self.vy = vy
        self.r = r
        self.m = m
    def change_attribute(self, x=None, y=None, vx=None, vy=None, r=None, m=None):
        if x!=None: self.x = x
        if y!=None: self.y = y
        if vx!=None: self.vx = vx
        if vy!=None: self.vy = vy
        if r!=None: self.r = r
        if m!=None: self.m = m

pygame.init()
screen = pygame.display.set_mode((500, 500))
x, y = random.randint(11, 489), random.randint(11, 489)
vx, vy = random.randint(-5, 5), random.randint(-5, 5)
animationTimer = pygame.time.Clock()
balls = []
num_balls = 2
for x in range(num_balls):
    balls.append(Ball(random.randint(11, 489), random.randint(11, 489), random.randint(-5, 5), random.randint(-5, 5), random.randint(30, 50), random.randint(1, 4)*5))
while True:
    for e in pygame.event.get():   
        if e.type == pygame.QUIT:
            break
    for ball in balls:
        pygame.draw.circle(screen, (200, 0, 0), (ball.x, ball.y), ball.r)
        if ball.x-ball.r<=0 or ball.x+ball.r>=500:
            ball.change_attribute(vx = -ball.vx)
        if ball.y-ball.r<=0 or ball.y+ball.r>=500:
            ball.change_attribute(vy = -ball.vy)
        for other in balls:
            if other != ball and checkcirclecollide(ball.x, ball.y, ball.r, other.x, other.y, other.r):
                new_v1x, new_v2x = ballcollision(ball.m, other.m, ball.vx, other.vx)
                new_v1y, new_v2y = ballcollision(ball.m, other.m, ball.vy, other.vy)
                ball.change_attribute(vx = new_v1x, vy = new_v1y)
                other.change_attribute(vx = new_v2x, vy = new_v2y)
        ball.change_attribute(x = ball.x+ball.vx, y = ball.y+ball.vy)
    animationTimer.tick(100)
    pygame.display.update()
    screen.fill((0, 0, 0))

Upvotes: 1

Views: 1124

Answers (1)

Carl HR
Carl HR

Reputation: 820

I did some modifications to your script. The modifications I made are explained on the script itself. As Kinetic Energy is outside of my field of expertise, I have no idea to say if it's really working, maybe it still needs a few modifications. But at least the balls do not merge or stick to the walls anymore.

Also, I added custom colors for each ball. They are much better for testing and see where and how they collide to each other.

import pygame
import random
import math

# From: http://www.jeffreythompson.org/collision-detection/circle-circle.php
def checkcirclecollide(x1, y1, r1, x2, y2, r2):
    distX = x1 - x2
    distY = y1 - y2
    distance = math.sqrt( (distX * distX) + (distY * distY) )

    return (distance <= (r1 + r2))

# Kinectic Energy.. I'm sorry, I have no idea what this does..
def ballcollision(m1, m2, v1, v2):
    v2f = (2*m1*v1 + m2*v2 - m1*v2) / (m1+m2)
    v1f = (m1*v1 + m2*v2 - m2*v2f)/m1
    return v1f, v2f

# When two balls collide, there's no guarantee they are touching
# by their extremities.
#
# As they have different speeds, they might enter into one's
# space when they collide. So before/after applying the kinetic energy,
# we must firstly separate the balls so they do not merge to
# each other.
#
# Basically this happens:
#   __  __
#  /  /\  \
#  \__\/__/
#
# What does it do? It separates each ball once they merge:
#   ___   ___
#  /   \ /   \
#  \___/ \___/
#
# This function should only be called when both balls are
# colliding to each other.
def separate_balls(ball, other):
    # Get the Opposite direction
    angle = - math.atan2(ball.y - other.y, ball.x - other.x)

    # Calculate distance between both balls
    distX = ball.x - other.x
    distY = ball.y - other.y
    distance = math.sqrt( (distX * distX) + (distY * distY) )
    diffR = ball.r + other.r - distance

    diffR *= 0.5

    # Separate each ball by half of distance
    ball.x += math.cos(angle) * diffR
    ball.y += math.sin(angle) * diffR

    other.x -= math.cos(angle) * diffR
    other.y -= math.sin(angle) * diffR

class Ball:
    def __init__(self, x, y, vx, vy, r, m, color):
        self.x = x
        self.y = y
        self.vx = vx
        self.vy = vy
        self.r = r
        self.m = m

        # This is better for testing
        self.color = color

    def change_attribute(self, x=None, y=None, vx=None, vy=None, r=None, m=None):
        if x!=None: self.x = x
        if y!=None: self.y = y
        if vx!=None: self.vx = vx
        if vy!=None: self.vy = vy
        if r!=None: self.r = r
        if m!=None: self.m = m

pygame.init()
screen = pygame.display.set_mode((500, 500))

x, y = random.randint(11, 489), random.randint(11, 489)
vx, vy = random.randint(-5, 5), random.randint(-5, 5)

animationTimer = pygame.time.Clock()
balls = []
num_balls = 2

# Now I added random colors!
for x in range(num_balls):
    balls.append(Ball(random.randint(11, 489), random.randint(11, 489), random.randint(-5, 5), random.randint(-5, 5), random.randint(30, 50), random.randint(1, 4)*5, (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))))

while True:
    for e in pygame.event.get():   
        if e.type == pygame.QUIT:
            break
    
    for ball in balls:
        pygame.draw.circle(screen, ball.color, (ball.x, ball.y), ball.r)
        
        if ball.x-ball.r-ball.vx<=0 or ball.x+ball.r+ball.vx>=500:

            # Avoid sticking at walls
            if (ball.x<=ball.r):
                ball.x-=(ball.vx-1)
            elif (ball.x>=500-ball.r):
                ball.x+=(ball.vx-1)

            ball.change_attribute(vx = -ball.vx)
        
        if ball.y-ball.r-ball.vy<=0 or ball.y+ball.r+ball.vy>=500:

            # Avoid sticking at walls
            if (ball.y<=ball.r):
                ball.y-=(ball.vy-1)
            elif (ball.y>=500-ball.r):
                ball.y+=(ball.vy-1)

            ball.change_attribute(vy = -ball.vy)
        
        for other in balls:
            if other != ball and checkcirclecollide(ball.x, ball.y, ball.r, other.x, other.y, other.r):
                new_v1x, new_v2x = ballcollision(ball.m, other.m, ball.vx, other.vx)
                new_v1y, new_v2y = ballcollision(ball.m, other.m, ball.vy, other.vy)



                # Save current position
                originalX = ball.x
                originalY = ball.y

                # Calculate a hint of next position
                lookAheadX = ball.x + ball.vx
                lookAheadY = ball.y + ball.vy

                # Push hint position
                ball.change_attribute(x = lookAheadX, y = lookAheadY)

                # Separate both balls from each other
                separate_balls(ball, other)

                # Pop hint position to original one
                ball.change_attribute(x = originalX, y = originalY)



                ball.change_attribute(vx = new_v1x, vy = new_v1y)
                other.change_attribute(vx = new_v2x, vy = new_v2y)
        
        ball.change_attribute(x = ball.x+ball.vx, y = ball.y+ball.vy)
    
    animationTimer.tick(100)
    pygame.display.update()
    screen.fill((0, 0, 0))

Also, this is the idea I used on separate_balls(). I'm not very good with Paint, but here we go: enter image description here

x on the picture is diffR on the script. I use this value to move each ball away from each other once they merge.

Upvotes: 1

Related Questions