2d Collisions
import asyncio
import random
import numpy as np
import pygame as pg
SimulationUpdates, SimElapesdTime, MaxSimulationSteps = 0, 0, 0
OLDCollidingPairs = []
pg.init()
clock = pg.time.Clock()
WIDTH, HEIGHT = 1400 , 800
screen = pg.display.set_mode((WIDTH, HEIGHT))
def Text(text,x,y,size):
font = pg.font.Font(None, size)
text = font.render(text, True, (255, 255, 255))
screen.blit(text, (x, y))
def circles_overlap(x1,y1,size1,x2,y2,size2):
dist = (x1-x2) * (x1-x2) + (y1-y2) * (y1-y2)
if dist <= (size1+size2) * (size1+size2):
return True
else:
return False
class LineSegment:
instances=[]
def __init__(self,sx,sy,ex,ey,rad):
self.sx = sx
self.sy = sy
self.ex = ex
self.ey = ey
self.radius = rad
class Ball:
instances=[]
def __init__(self, x, y, size):
self.x = x
self.y = y
self.vx = 0
self.vy = 0
self.ax = 0
self.ay = 0
self.size = size
col=random.randint(100,200)
self.colour = (20,col,col)
self.mass = size * 10
self.SimTimeRemaining = 0
self.oldx = 0
self.oldy = 0
col=random.randint(1,3)
if col==1:
self.colour=(255,0,0)
elif col==2:
self.colour=(0,255,0)
else:
self.colour=(0,0,255)
def Collide(self):
collided=False
for ball in Ball.instances:
if ball != self:
if circles_overlap(self.x,self.y,self.size, ball.x,ball.y,ball.size):
dist = ((self.x-ball.x)*(self.x-ball.x) + (self.y-ball.y)*(self.y-ball.y)) ** 0.5
overlap = 0.5 * (dist - self.size - ball.size)
#displace self
self.x -= overlap * (self.x - ball.x) / dist
self.y -= overlap * (self.y - ball.y) / dist
#displace target
ball.x += overlap * (self.x - ball.x) / dist
ball.y += overlap * (self.y - ball.y) / dist
return Ball.instances.index(self), Ball.instances.index(ball)
collided=True
#if collided:#time displacement
try:
IntendedSpeed = (self.vx*self.vx + self.vy*self.vy)**0.5
IntendedDistance = IntendedSpeed * self.SimTimeRemaining
ActualDistance = ((self.x-self.oldx)*(self.x-self.oldx) + (self.y-self.oldy)*(self.y-self.oldy))**0.5
ActualTime = ActualDistance / IntendedSpeed
self.SimTimeRemaining = self.SimTimeRemaining - ActualTime
except:
pass
def Move(self):
if self.SimTimeRemaining > 0:
self.oldx = self.x
self.oldy = self.y
#drag to simulate rolling friction
self.ax = -self.vx * 0.08
self.ay = -self.vy * 0.08 + gravity #gravity
#update ball physics
self.vx += self.ax * self.SimTimeRemaining / TargetFPS
self.vy += self.ay * self.SimTimeRemaining / TargetFPS
self.x += self.vx * self.SimTimeRemaining / TargetFPS
self.y += self.vy * self.SimTimeRemaining / TargetFPS
#move to other side of screen
if self.x <0:
self.x += WIDTH
if self.x >= WIDTH:
self.x -= WIDTH
if self.y <0:
self.y += HEIGHT
if self.y >= HEIGHT:
self.y -= HEIGHT
if self.vx*self.vx + self.vy*self.vy < 0.0001:
self.vx = 0
self.vy = 0
return self.Collide()
def MoveAll():
global OLDCollidingPairs
CollNoise = False
BopNoise = False
for i in range(SimulationUpdates):
for ball in Ball.instances:
ball.SimTimeRemaining = SimElapesdTime
for j in range(MaxSimulationSteps):
CollidingPairs=[]
FakeBalls=[]
#static collisions
for ball in Ball.instances:
isfake = False
for i in range(len(FakeBalls)):
if Ball.instances.index(FakeBalls[i]) == Ball.instances.index(ball):
isfake = True
if not isfake:
coll = ball.Move()
if coll!=None:
CollidingPairs.append(coll)
for edge in LineSegment.instances:
LineX1 = edge.ex - edge.sx
LineY1 = edge.ey - edge.sy
LineX2 = ball.x - edge.sx
LineY2 = ball.y - edge.sy
EdgeLength = LineX1*LineX1 + LineY1*LineY1
t = max(0, min(EdgeLength, (LineX1*LineX2 + LineY1*LineY2))) / EdgeLength
ClosestPointX = edge.sx + t * LineX1
ClosestPointY = edge.sy + t * LineY1
Distance = ((ball.x- ClosestPointX)*(ball.x- ClosestPointX) + (ball.y - ClosestPointY)*(ball.y - ClosestPointY))**0.5
if Distance <= ball.size + edge.radius:
#static collision occoured
Ball.instances.append(Ball(ClosestPointX,ClosestPointY,edge.radius))
fakeball = Ball.instances[-1]
fakeball.mass = ball.mass * 1
fakeball.vx = -ball.vx
fakeball.vy = -ball.vy
#fakeball.mass = ball.mass * 1
#fakeball.vx = -ball.vx
#fakeball.vy = -ball.vy
FakeBalls.append(fakeball)
coll = Ball.instances.index(ball), Ball.instances.index(fakeball)
CollidingPairs.append(coll)
overlap = 1 * (Distance - ball.size - fakeball.size)
#displace target
ball.x -= overlap * (ball.x - fakeball.x) / Distance
ball.y -= overlap * (ball.y - fakeball.y) / Distance
#dynamic collisions
for i in range(len(CollidingPairs)):
b1 = Ball.instances[CollidingPairs[i][0]]
b2 = Ball.instances[CollidingPairs[i][1]]
#distance
dist = ((b1.x-b2.x)*(b1.x-b2.x) + (b1.y-b2.y)*(b1.y-b2.y)) ** 0.5
#normal
nx = (b2.x - b1.x) / dist
ny = (b2.y - b1.y) / dist
#tangent
tx = -ny
ty = nx
#dot product tangent
dpTan1 = b1.vx * tx + b1.vy * ty
dpTan2 = b2.vx * tx + b2.vy * ty
#dot product normal
dpNorm1 = b1.vx * nx + b1.vy * ny
dpNorm2 = b2.vx * nx + b2.vy * ny
#conservation of momentum in 1D
m1 = (dpNorm1 * (b1.mass - b2.mass) + 2 * b2.mass * dpNorm2) / (b1.mass + b2.mass)
m2 = (dpNorm2 * (b2.mass - b1.mass) + 2 * b1.mass * dpNorm1) / (b1.mass + b2.mass)
#update velocitys
b1.vx = tx * dpTan1 + nx * m1
b1.vy = ty * dpTan1 + ny * m1
b2.vx = tx * dpTan2 + nx * m2
b2.vy = ty * dpTan2 + ny * m2
IntendedSpeed1 = (b1.vx*b1.vx + b1.vy*b1.vy)**0.5
IntendedSpeed2 = (b2.vx*b2.vx + b2.vy*b2.vy)**0.5
for fakeball in FakeBalls:
Ball.instances.remove(fakeball)
CollidingPairs=[]
FakeBalls=[]
def DrawAll(mousemoving2,mouse_x,mouse_y):
screen.fill((0,0,0))
for ball in Ball.instances:
pg.draw.circle(screen,ball.colour,(ball.x,ball.y),ball.size)
for line in LineSegment.instances:
pg.draw.circle(screen,(200,255,200),(line.sx,line.sy),line.radius,5)
pg.draw.circle(screen,(200,255,200),(line.ex,line.ey),line.radius,5)
nx = -(line.ey - line.sy)
ny = (line.ex - line.sx)
d = (nx*nx + ny*ny)**0.5
nx /= d
ny /= d
rad = line.radius-5/2
pg.draw.line(screen,(200,255,200),(line.sx + nx*rad, line.sy + ny*rad), (line.ex + nx*rad, line.ey + ny*rad),5)
pg.draw.line(screen,(200,255,200),(line.sx - nx*rad, line.sy - ny*rad), (line.ex - nx*rad, line.ey - ny*rad),5)
if mousemoving2!=-1:
pg.draw.line(screen,(0,0,255),(int(Ball.instances[mousemoving2].x), int(Ball.instances[mousemoving2].y)),(mouse_x,mouse_y),5)
async def Main():
global SimulationUpdates, SimElapesdTime, MaxSimulationSteps
mousemoving0=-1
mousemoving2=-1
while True:
SimulationUpdates = 4
MaxSimulationSteps = 10
MaxSimulationSteps = 5
SimElapesdTime = clock.get_fps() / SimulationUpdates / MaxSimulationSteps
for event in pg.event.get():
if event.type == pg.QUIT:
exit()
key = pg.key.get_pressed()
mouse_x, mouse_y = pg.mouse.get_pos()
mouse_pressed = pg.mouse.get_pressed()
#left click
if mouse_pressed[0]:
if mousemoving0==-1:
for ball in Ball.instances:
if circles_overlap(ball.x,ball.y,ball.size,mouse_x,mouse_y,0):
mousemoving0=Ball.instances.index(ball)
movingtype="Ball"
for line in LineSegment.instances:
if circles_overlap(line.sx,line.sy,line.radius,mouse_x,mouse_y,0):
mousemoving0=line.instances.index(line)
movingtype="SLine"
if circles_overlap(line.ex,line.ey,line.radius,mouse_x,mouse_y,0):
mousemoving0=line.instances.index(line)
movingtype="ELine"
else:
mousemoving0=-1
if mousemoving0!=-1:
if movingtype == "Ball":
Ball.instances[mousemoving0].x = mouse_x
Ball.instances[mousemoving0].y = mouse_y
elif movingtype == "SLine":
LineSegment.instances[mousemoving0].sx = mouse_x
LineSegment.instances[mousemoving0].sy = mouse_y
elif movingtype == "ELine":
LineSegment.instances[mousemoving0].ex = mouse_x
LineSegment.instances[mousemoving0].ey = mouse_y
#right click
if mouse_pressed[2]:
if mousemoving2==-1:
for ball in Ball.instances:
if circles_overlap(ball.x,ball.y,ball.size,mouse_x,mouse_y,0):
mousemoving2=Ball.instances.index(ball)
else:
if mousemoving2!=-1:
Ball.instances[mousemoving2].vx = 0.2 * (Ball.instances[mousemoving2].x - mouse_x)
Ball.instances[mousemoving2].vy = 0.2 * (Ball.instances[mousemoving2].y - mouse_y)
mousemoving2=-1
#move everything
MoveAll()
#draw everything
DrawAll(mousemoving2,mouse_x,mouse_y)
pg.display.set_caption(f"2d Collisions - left click+drag circles to move them - right click+drag to add velocity to ball")
pg.display.flip()
clock.tick(TargetFPS)
await asyncio.sleep(0)
#for line in LineSegment.instances:
# print(f"LineSegment({line.sx},{line.sy},{line.ex},{line.ey},20)")
TargetFPS=60
gravity = 1
LineSegment.instances.append(LineSegment(873,706,598,640,20))
LineSegment.instances.append(LineSegment(5,80,332,205,20))
LineSegment.instances.append(LineSegment(989,324,1371,152,20))
LineSegment.instances.append(LineSegment(340,402,609,417,20))
for i in range(50):
Ball.instances.append(Ball(random.randint(50,WIDTH-50),random.randint(50,HEIGHT-50),random.randint(20,40)))
asyncio.run(Main())