Search code examples
pythontkintersimulationphysicsmodeling

Bouncing 2D balls - Python


I am trying to making a program where there are 6 sections and a bunch of balls bounce of each other. The balls are not doing what they are supposed to be doing and instead getting stuck on each other. Sometimes they bounce, other times they don't.

I have tried multiple formulas for 2D bouncing but this once seems to actually preserve momentum. This is an altered model from what I had previously which was basically a direct copy from the wikipedia page for 2D collisions. That didn't work and the current model I took from another stackoverflow answer and produces the same results. I'm thinking my physics might be right but something else in the code is messing it up. I've looked it over and over again but I still can't seem to find what that could be. Any help would be appreciated.

from tkinter import *
import random

tk = Tk()

#canvas object
canv_width = 300
canv_height = 300


canvas1 = Canvas(tk, width = canv_width, height = canv_height, bg = 'black',highlightthickness=1, highlightbackground='white')
canvas2 = Canvas(tk, width = canv_width, height = canv_height, bg = 'black',highlightthickness=1, highlightbackground='white')
canvas3 = Canvas(tk, width = canv_width, height = canv_height, bg = 'black',highlightthickness=1, highlightbackground='white')
canvas4 = Canvas(tk, width = canv_width, height = canv_height, bg = 'black',highlightthickness=1, highlightbackground='white')
canvas5 = Canvas(tk, width = canv_width, height = canv_height, bg = 'black',highlightthickness=1, highlightbackground='white')
canvas6 = Canvas(tk, width = canv_width, height = canv_height, bg = 'black',highlightthickness=1, highlightbackground='white')

canvas1.grid(row=0,column=0)
canvas2.grid(row=0,column=1)
canvas3.grid(row=1,column=0)
canvas4.grid(row=1,column=1)
canvas5.grid(row=2,column=0)
canvas6.grid(row=2,column=1)
tk.title("BounceBox")
towns = {'1': canvas1, '2' : canvas2, '3': canvas3, '4' : canvas4, '5' : canvas5, '6': canvas6}
pops = {'1' : [], '2': [], '3': [], '4': [], '5': [], '6':[]}

def subtract(v1,v2):
    x_comp = v1[0] - v2[0]
    y_comp = v1[1] - v2[1]
    return ((x_comp,y_comp))

def add(v1,v2):
    x_comp = v1[0] + v2[0]
    y_comp = v1[1] + v2[1]
    return((x_comp,y_comp))


def dotproduct(v1,v2):
    x_comp = v1[0] * v2[0]
    y_comp = v1[1] * v2[1]
    return(x_comp + y_comp)

def magnitude(v):
    mag = (v[0]**2 + v[1]**2)**0.5
    return mag

def mult_sv(s,v):
    x_comp = s*v[0]
    y_comp = s*v[1]
    return((x_comp,y_comp))

def arctan(y,x):
    try:
        ans = math.atan(y/x)
        if x >= 0 and y >= 0:
            return ans
        elif x <= 0 and y >= 0:
            return ans + math.pi
        elif x >= 0 and y <= 0:
            return ans
        else:
            return ans - math.pi
    except:
        if y>0:
            return math.pi
        else:
            return -math.pi

class Ball:

    def __init__(self,radius,color,location,location_name):
        self.location = location
        self.location_name = location_name
        self.radius = radius
        ball_x = random.randint(0,canv_width - radius)
        ball_y = random.randint(0,canv_height - radius)
        self.pos = (ball_x,ball_y,ball_x + 2*self.radius, ball_y + 2*self.radius)
        self.center = (self.pos[0] + self.radius, self.pos[1] + self.radius)
        self.xspeed = random.randint(-1,1)
        self.yspeed = random.randint(-1,1)


        self.color = color
        self.shape = self.location.create_oval(self.pos[0],self.pos[1],self.pos[2], self.pos[3], fill = self.color)
        self.ball_update()

    def check_collision(self):
        for person in pops[self.location_name]:
            center = person.center
            distance = ((self.center[0] - person.center[0])**2 + (self.center[1] - person.center[1])**2)**0.5
            if (distance <= 2*self.radius and person != self):
                return (True,person)
        return (False,None)


    def ball_update(self):
        #print(arctan(-1,1))
        self.pos = self.location.coords(self.shape)
        self.center = (self.pos[0] + self.radius, self.pos[1] + self.radius)
        if (self.pos[0] <= 0 or self.pos[2] >= canv_width):
            self.xspeed = -self.xspeed
        if (self.pos[1] <= 0 or self.pos[3] >= canv_height):
            self.yspeed = -self.yspeed
        collision = self.check_collision()
        if collision[0] == True:
            v1 = (self.xspeed,self.yspeed)
            v2 = (collision[1].xspeed, collision[1].yspeed)

            dist = subtract(self.center,collision[1].center)
            v12 = subtract(v1,v2)
            dv = mult_sv(dotproduct(v12,dist)/ magnitude(dist)**2,dist)
            self.xspeed -= dv[0]
            self.yspeed -= dv[1]
            collision[1].xspeed += dv[0]
            collision[1].yspeed += dv[1]

        self.location.move(self.shape, self.xspeed, self.yspeed)

        self.center = (self.pos[0] + self.radius, self.pos[1] + self.radius)
        tk.after(15,self.ball_update)

def create_populations():
    for town in towns:
        for pop in range(5):
            person = Ball(10,'white',towns[town],town)
            pops[town].append(person)

create_populations()
tk.mainloop()

Solution

  • Consider the following situation: 2 balls get close to each other, you detect the collision, recalculate their speeds, and go to next frame, but in the next frame these two balls are still very close to each other. Then you'll detect collision between them again recalculate their speeds (reverting it back to original values), go to next frame. Balls are close to each other again, again collide, etc.

    In other words, you need to disable collision detection for two balls after they just collided to let them get away from each other. There are different ways to do it. For example, you can make your collision detection more complex. You can actually consider the directions in which two balls are moving, find the point of intersection of their straight-line trajectories. If this point of intersection lies in their past - then don't count them as colliding. If the point of intersection is in their future - then perform the proximity test like you are doing now.