Bouncing_circles - brute force collision detection example in Python
Created by ChatGPT, runs without errors. I just tweaked a few parameters.
Same group → attraction (force pulls circles together)
Opposite group → repulsion (force pushes circles apart)
Moving the center of gravity, bouncing off the screen edges.
High central pull → chaos and swirling behavior.
Elastic collisions between circles and walls are preserved.
Mass-based attraction within groups.
Flocking: cohesion, alignment, separation.
Colors are coherent within groups.
*****************************************************************
Python code - just copy-paste into a txt file bouncing_circles.py.
pip install pygame
Run with: python bouncing_circles.py
*****************************************************************
# --code begins here--
import pygame
import random
import math
# --- Configuration ---
WIDTH, HEIGHT = 900, 700
NUM_CIRCLES = 200
MIN_RADIUS = 3
MAX_RADIUS = 6
MAX_SPEED =3
GROUPS = 7
FORCE_SCALE = -200
ALIGNMENT_SCALE = 0.005
COHESION_SCALE = 0.003
CENTER_GRAVITY = 25 # you wanted this high — now used as a force magnitude, not multiplied by distance
TRAIL_FADE = 210
# --- Initialize ---
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
#trail_surface = pygame.Surface((WIDTH, HEIGHT))
trail_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
# Base colors per group
BASE_COLORS = [
(255, 80, 80), #
(80, 255, 80), #
(80, 80, 255), #
(255, 255, 80), #
(255, 80, 255), #
(10, 80, 55), #
(110, 10, 200), #
]
# --- Circle class ---
class Circle:
def __init__(self, group):
self.group = group
self.radius = random.choices(
population=[MIN_RADIUS, 8, 10, 12, 15, 20, MAX_RADIUS],
weights=[50, 15, 10, 10, 7, 5, 3],
k=1
)[0]
self.mass = max(1.0, self.radius ** 2) # avoid zero mass
self.x = random.uniform(self.radius, WIDTH - self.radius)
self.y = random.uniform(self.radius, HEIGHT - self.radius)
self.vx = random.uniform(-MAX_SPEED, MAX_SPEED)
self.vy = random.uniform(-MAX_SPEED, MAX_SPEED)
r, g, b = BASE_COLORS[group]
self.color = (
min(255, max(0, r + random.randint(-40, 40))),
min(255, max(0, g + random.randint(-40, 40))),
min(255, max(0, b + random.randint(-40, 40))),
)
def move(self):
self.x += self.vx
self.y += self.vy
# Smooth speed limiting
speed = math.hypot(self.vx, self.vy)
if speed > MAX_SPEED:
scale = MAX_SPEED / speed
self.vx *= scale
self.vy *= scale
# Bounce off walls
if self.x < self.radius or self.x > WIDTH - self.radius:
self.vx *= -4
self.x = max(self.radius, min(WIDTH - self.radius, self.x))
if self.y < self.radius or self.y > HEIGHT - self.radius:
self.vy *= -4
self.y = max(self.radius, min(HEIGHT - self.radius, self.y))
def draw(self, surf):
glow_intensity = max(40, min(255, int(math.sqrt(self.mass) * 2)))
color = (*self.color, glow_intensity)
pygame.draw.circle(surf, color, (int(self.x), int(self.y)), self.radius)
# --- Moving center of gravity ---
class MovingCenter:
def __init__(self, x, y):
self.x = x
self.y = y
self.vx = random.choice([-5, 5])
self.vy = random.choice([-5, 5])
def move(self):
self.x += self.vx
self.y += self.vy
if self.x < 0 or self.x > WIDTH:
self.vx *= -1
self.x = max(0, min(WIDTH, self.x))
if self.y < 0 or self.y > HEIGHT:
self.vy *= -1
self.y = max(0, min(HEIGHT, self.y))
# --- Forces ---
def handle_forces(circles, center):
# Compute group centers
group_centers = [[0, 0, 0] for _ in range(GROUPS)]
for c in circles:
gx, gy, count = group_centers[c.group]
group_centers[c.group] = [gx + c.x, gy + c.y, count + 1]
group_positions = [(gx/count, gy/count) if count > 0 else (0, 0) for gx, gy, count in group_centers]
# Pairwise forces + collisions
for i in range(len(circles)):
for j in range(i + 1, len(circles)):
c1 = circles[i]
c2 = circles[j]
dx = c2.x - c1.x
dy = c2.y - c1.y
dist = math.hypot(dx, dy)
if dist == 0:
# jitter slightly to avoid exact overlap numeric issues
dx = (random.random() - 0.5) * 0.01
dy = (random.random() - 0.5) * 0.01
dist = math.hypot(dx, dy)
nx, ny = dx / dist, dy / dist
if c1.group == c2.group:
# gravitational-like attraction (force magnitude falls with dist^2)
force = FORCE_SCALE * (c1.mass + c2.mass) / (dist**2 + 1e-6)
fx = nx * force
fy = ny * force
else:
# repulsion proportional to size
repulsion = 0.02 * (c1.radius + c2.radius)
fx = -nx * repulsion
fy = -ny * repulsion
# apply equal and opposite impulses (convert force -> acceleration)
c1.vx += fx / c1.mass
c1.vy += fy / c1.mass
c2.vx -= fx / c2.mass
c2.vy -= fy / c2.mass
# Collision bounce (elastic along normal)
min_dist = c1.radius + c2.radius
if dist < min_dist:
overlap = min_dist - dist
c1.x -= nx * overlap / 2
c1.y -= ny * overlap / 2
c2.x += nx * overlap / 2
c2.y += ny * overlap / 2
# relative velocity along normal
rel = (c1.vx * nx + c1.vy * ny) - (c2.vx * nx + c2.vy * ny)
if rel < 0: # only if closing
# compute impulse scalar for elastic collision (1D along normal)
# using masses: j = -(1+e)*rel / (1/m1 + 1/m2); e=1 elasticity
j = -(1+1.0) * rel / (1.0 / c1.mass + 1.0 / c2.mass)
c1.vx += (j * nx) / c1.mass
c1.vy += (j * ny) / c1.mass
c2.vx -= (j * nx) / c2.mass
c2.vy -= (j * ny) / c2.mass
# Flocking + center pull
for c in circles:
gx, gy = group_positions[c.group]
# cohesion (toward group center)
c.vx += (gx - c.x) * COHESION_SCALE
c.vy += (gy - c.y) * COHESION_SCALE
# alignment (nearby same-group neighbors)
neighbor_vx, neighbor_vy, count = 0, 0, 0
for other in circles:
if other.group == c.group and other is not c:
dx = other.x - c.x
dy = other.y - c.y
dist = math.hypot(dx, dy)
if dist < 60:
neighbor_vx += other.vx
neighbor_vy += other.vy
count += 1
if count > 0:
avg_vx = neighbor_vx / count
avg_vy = neighbor_vy / count
c.vx += (avg_vx - c.vx) * ALIGNMENT_SCALE
c.vy += (avg_vy - c.vy) * ALIGNMENT_SCALE
# --- FIXED central gravity ---
# Use normalized direction to the moving center, then compute acceleration = force / mass
dx = center.x - c.x
dy = center.y - c.y
dist = math.hypot(dx, dy)
if dist > 0:
dirx, diry = dx / dist, dy / dist
# force magnitude is constant (CENTER_GRAVITY) but we can cap effect for stability
# and convert force -> acceleration by dividing by mass
force_mag = CENTER_GRAVITY
# optional cap: if center is extremely close, avoid huge accelerations
# accel = (dir * force_mag) / mass
ax = dirx * force_mag / c.mass
ay = diry * force_mag / c.mass
c.vx += ax
c.vy += ay
# --- Main loop ---
circles = []
for g in range(GROUPS):
circles.extend([Circle(g) for _ in range(NUM_CIRCLES // GROUPS)])
center = MovingCenter(WIDTH / 2, HEIGHT / 2)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Fade trails
#screen.fill((50, 50, 0))
trail_surface.fill((1, 8, 1, TRAIL_FADE))
# Move center then compute forces
center.move()
handle_forces(circles, center)
for c in circles:
c.move()
c.draw(trail_surface)
# Render
screen.blit(trail_surface, (0, 0))
#pygame.draw.circle(screen, (255, 255, 255), (int(center.x), int(center.y)), 6)
pygame.display.flip()
clock.tick(60)
pygame.quit()
No comments:
Post a Comment