Search code examples
pythongame-physicsphysics

2D Elastic Collision, momentum not conserved in the y direction


I am a high school student trying to model a 2D Elastic Collision between two billiard balls. I used vector rotation as shown in this video and I also referenced this website as well. Despite that, the collisions weren't working specifically when the two balls collide having the same x value (a vertical collision).

As I tried to fix that edge case issue, I noticed that the change in momentum in the y direction gets larger and larger as the collision vector becomes more vertical, when it should be zero. This issue is only present with momentum in the y direction, not in the x direction.

My code (apologies in advance if it's too convoluted):

#main.py

import pygame
import numpy as np
from math import sin, cos, atan2, degrees as deg

g = 9.81
m = .17
r = 10
timeInterval = 1/60


def getValue(value):
    absValue = np.abs(value)/value
    return absValue


def checkCollison(Ball):
    global prior
    collided = []
    instances = Ball.instances


    for a in range(len(instances)):
        for b in range(len(instances) - 1, a, -1):
            ballA, ballB = instances[a], instances[b]
            dist = ((ballA.x - ballB.x)**2 + (ballA.y - ballB.y)**2 + (ballA.z - ballB.z)**2)**0.5
            if dist <= 2*r and not(f"{a}{b}" in collided) and dist != 0:
                BallCollision(ballA, ballB, (ballA.vx, ballA.vy), (ballB.vx, ballB.vy))

                collided.append(f"{a}{b}")

def BallCollision(b1, b2, u1, u2):

    deltaX = b2.x - b1.x
    deltaY = b2.y - b1.y

    angle = atan2(deltaY, deltaX)
    print(deg(angle))

    # Rotate it to where the collision line is parallel to the horizontal
    u1x = b1.vx * cos(angle) + b1.vy * sin(angle)
    u1y = b1.vy * cos(angle) - b1.vx * sin(angle)
    u2x = b2.vx * cos(angle) + b2.vy * sin(angle)
    u2y = b2.vy * cos(angle) - b2.vx * sin(angle)

    v1x = ((b1.m - b2.m) / (b1.m + b2.m)) * u1x + ((2 * b2.m) / (b1.m + b2.m)) * u2x
    v1y = u1y

    v2x = ((2 * b1.m) / (b1.m + b2.m)) * u1x + ((b2.m - b1.m) / (b1.m + b2.m)) * u2x
    v2y = u2y

    midpointX = (b1.x + b2.x) / 2
    midpointY = (b1.y + b2.y) / 2

    b1.x += (b1.x - midpointX)/2
    b1.y += (b1.y - midpointY)/2
    b2.x += (b2.x - midpointX)/2
    b2.y += (b2.y - midpointY)/2

    #Rotate back
    v1x = v1x * cos(angle) - v1y * sin(angle)
    v1y = v1y * cos(angle) + v1x * sin(angle)
    v2x = v2x * cos(angle) - v2y * sin(angle)
    v2y = v2y * cos(angle) + v2x * sin(angle)

    print("change in x momentum: ", b1.vx + b2.vx - v1x - v2x)
    print("change in y momentum: ", b1.vy + b2.vy - v1y - v2y)


    b1.vx, b1.vy = v1x, v1y
    b2.vx, b2.vy = v2x, v2y


class Ball(pygame.sprite.Sprite):
    instances = []
    def __init__(self, x, y, z, vx, vy, vz, ax, ay, az, m):
        super(Ball, self).__init__()
        self.__class__.instances.append(self)
        self.x, self.y, self.z = x, y, z
        self.vx, self.vy, self.vz = vx, vy, vz
        self.ax, self.ay, self.az = ax, ay, az
        self.m = m

    def motion(self):
        Ax = 0 
        Ay = 0 

        self.vx = self.vx + Ax * timeInterval
        self.vy = self.vy + Ay * timeInterval
        self.vz = self.vy #+ Az * timeInterval
        self.x = self.x + self.vx * timeInterval + 1 / 2 * (Ax) * (timeInterval ** 2)
        self.y = self.y + self.vy * timeInterval + 1 / 2 * (Ay) * (timeInterval ** 2)
        self.z = self.y + self.vy * timeInterval #+ 1 / 2 * (Ay) * (timeInterval ** 2)

        checkCollison(Ball)
#game.py

from main import *

WIDTH, HEIGHT = 1000, 500
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
FPS = 60
WHITE  = (255, 255, 255)

ball1 = Ball(*(100,100,0,0,100,0,0,0,0, m))
ball2 = Ball(*(115,200,0,0,0,0,0,0,0, m))

def draw_window():
    WIN.fill(WHITE)

    ball1.motion()
    ball2.motion()

    pygame.draw.circle(WIN, (0,0,0), (ball1.x,ball1.y), r)
    pygame.draw.circle(WIN, (0,0,0), (ball2.x,ball2.y), r)

    pygame.display.update()

def main():
    clock = pygame.time.Clock()
    run = True
    while run:
        clock.tick(FPS)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
        draw_window()

    pygame.quit()

if __name__ == "__main__":
    main()

Solution

  • The problem with your code lies in the way you rotate back your velocities in your BallCollision function:

    #Rotate back
    v1x = v1x * cos(angle) - v1y * sin(angle)
    v1y = v1y * cos(angle) + v1x * sin(angle)
    v2x = v2x * cos(angle) - v2y * sin(angle)
    v2y = v2y * cos(angle) + v2x * sin(angle)
    

    In these lines you overwrite your variables for v1x and v2x but still try to use them in the next line for your rotation. However the fix is of course not difficult, you just need to use different variables names. For example the following code fixed "vertical" collisions for me:

    #Rotate back
    newv1x = v1x * cos(angle) - v1y * sin(angle)
    newv1y = v1y * cos(angle) + v1x * sin(angle)
    newv2x = v2x * cos(angle) - v2y * sin(angle)
    newv2y = v2y * cos(angle) + v2x * sin(angle)
    
    print("change in x momentum: ", b1.vx + b2.vx - newv1x - newv2x)
    print("change in y momentum: ", b1.vy + b2.vy - newv1y - newv2y)
    
    
    b1.vx, b1.vy = newv1x, newv1y
    b2.vx, b2.vy = newv2x, newv2y