DEV Community

Cover image for Simulating Bouncing Balls with Collisions in Python using Pygame
Dmitry Romanoff
Dmitry Romanoff

Posted on

Simulating Bouncing Balls with Collisions in Python using Pygame

In this article, we will walk through a Python program that simulates bouncing balls within a window, using the Pygame library. The code also includes functionality for detecting and resolving collisions between the balls. This example provides a good introduction to both basic physics in programming and how to use Pygame to create dynamic simulations.

Simulating Bouncing Balls with Collisions in Python using Pygame

What You'll Learn

  • Setting up Pygame for creating a simulation.
  • Implementing basic motion of balls with random velocity.
  • Detecting collisions between balls and resolving them according to physical principles.
  • Using object-oriented programming (OOP) in Python to structure the simulation.

Setting Up the Environment

Before diving into the code, we need to ensure that Pygame is installed. If you haven’t installed Pygame yet, you can do so using pip:

pip install pygame
Enter fullscreen mode Exit fullscreen mode

Understanding the Code

Here’s an overview of how the program works:

1. Pygame Initialization and Display Setup

pygame.init()

# Set up the display
width = 800
height = 600
ball_radius = 20
screen = pygame.display.set_mode((width, height))
clock = pygame.time.Clock()
Enter fullscreen mode Exit fullscreen mode

The code begins by initializing Pygame and setting up the display. The window dimensions are 800x600 pixels, and each ball in the simulation has a radius of 20 pixels.

2. Defining Colors

white = (255, 255, 255)
black = (0, 0, 0)
red = (255, 0, 0)
green = (0, 255, 0)
blue = (0, 0, 255)
Enter fullscreen mode Exit fullscreen mode

These are RGB color definitions used to render balls and the background. In this case, we have three colors for the balls (red, green, blue) and two colors for the background (white and black).

3. Ball Class: Managing Individual Balls

class Ball:
    def __init__(self, color):
        self.x = random.randint(ball_radius, width - ball_radius)
        self.y = random.randint(ball_radius, height - ball_radius)
        self.velocity_x = random.randint(1, 3) * 2
        self.velocity_y = random.randint(1, 3) * -2
        self.color = color
Enter fullscreen mode Exit fullscreen mode

The Ball class is central to this program. Each ball object has properties like position (x, y), velocity (velocity_x, velocity_y), and color. The ball's initial position and velocity are set randomly, so each ball starts in a different location and moves at a unique speed.

4. Moving the Balls and Handling Boundary Bounces

def move(self):
    self.x += self.velocity_x
    self.y += self.velocity_y

    # Bounce off walls, considering the radius of the ball
    if self.x <= ball_radius or self.x >= width - ball_radius:
        self.velocity_x *= -1
        if self.x <= ball_radius:
            self.x = ball_radius
        elif self.x >= width - ball_radius:
            self.x = width - ball_radius

    if self.y <= ball_radius:
        self.velocity_y *= -1
        self.y = ball_radius
    elif self.y >= height - ball_radius:
        self.velocity_y *= -1
        self.y = height - ball_radius
Enter fullscreen mode Exit fullscreen mode

The move method updates the ball's position based on its velocity. The program handles wall collisions by reversing the velocity along the respective axis when a wall is hit, making the ball bounce off the walls.

5. Drawing the Balls on the Screen

def draw(self):
    pygame.draw.circle(screen, self.color, (self.x, self.y), ball_radius)
Enter fullscreen mode Exit fullscreen mode

The draw method uses Pygame’s pygame.draw.circle() function to render each ball on the screen with its corresponding color and position.

6. Handling Ball-to-Ball Collisions

def check_collision(self, other_ball):
    dx = self.x - other_ball.x
    dy = self.y - other_ball.y
    distance = math.sqrt(dx**2 + dy**2)

    # If the distance between centers is less than twice the radius, a collision occurred
    if distance < 2 * ball_radius:
        # Normalize the vector between the two balls
        normal_vector = pygame.math.Vector2(dx, dy).normalize()

        # Calculate relative velocity
        relative_velocity = pygame.math.Vector2(self.velocity_x, self.velocity_y) - pygame.math.Vector2(other_ball.velocity_x, other_ball.velocity_y)

        # Calculate the velocity along the normal vector (dot product)
        velocity_along_normal = relative_velocity.dot(normal_vector)

        if velocity_along_normal > 0:
            return

        # Calculate the impulse scalar
        impulse = 2 * velocity_along_normal / (2 * ball_radius)

        # Apply the impulse to the velocities of both balls
        self.velocity_x -= impulse * normal_vector.x
        self.velocity_y -= impulse * normal_vector.y
        other_ball.velocity_x += impulse * normal_vector.x
        other_ball.velocity_y += impulse * normal_vector.y

        # Reposition the balls to avoid overlap
        overlap = 2 * ball_radius - distance
        self.x += normal_vector.x * overlap / 2
        self.y += normal_vector.y * overlap / 2
        other_ball.x -= normal_vector.x * overlap / 2
        other_ball.y -= normal_vector.y * overlap / 2
Enter fullscreen mode Exit fullscreen mode

The check_collision method handles the detection of collisions between two balls. It calculates the distance between the centers of two balls. If the distance is less than the sum of their radii (i.e., they overlap), the program applies basic physics principles to resolve the collision by adjusting the balls' velocities and positions. This ensures that the balls bounce off each other in a realistic manner.

7. The Main Game Loop

balls = [Ball(red), Ball(green), Ball(blue)]
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Move and check collisions for each ball
    for ball in balls:
        ball.move()

    # Check for collisions between the balls
    for i in range(len(balls)):
        for j in range(i + 1, len(balls)):
            balls[i].check_collision(balls[j])

    # Draw the window
    screen.fill(white)

    # Draw all balls
    for ball in balls:
        ball.draw()

    # Update and render
    pygame.display.flip()
    clock.tick(60)
Enter fullscreen mode Exit fullscreen mode

The main game loop continuously updates the positions of the balls, checks for collisions, and renders the screen. The window is filled with a white background at the start of each loop iteration, and then each ball is drawn on top. The loop runs at 60 frames per second (clock.tick(60)).

8. Exiting the Program

pygame.quit()
Enter fullscreen mode Exit fullscreen mode

When the user closes the window, Pygame shuts down and the program terminates.

The Full Code

Here is the full code for the simulation:

import pygame
import random
import math

# Initialize Pygame
pygame.init()

# Set up the display
width = 800
height = 600
ball_radius = 20

# Define colors
white = (255, 255, 255)
black = (0, 0, 0)
red = (255, 0, 0)
green = (0, 255, 0)
blue = (0, 0, 255)

# Create a Pygame display surface
screen = pygame.display.set_mode((width, height))
clock = pygame.time.Clock()

# Ball class to manage multiple balls
class Ball:
    def __init__(self, color):
        self.x = random.randint(ball_radius, width - ball_radius)
        self.y = random.randint(ball_radius, height - ball_radius)
        self.velocity_x = random.randint(1, 3) * 2
        self.velocity_y = random.randint(1, 3) * -2
        self.color = color

    def move(self):
        self.x += self.velocity_x
        self.y += self.velocity_y

        # Bounce off walls, considering the radius of the ball
        if self.x <= ball_radius or self.x >= width - ball_radius:
            self.velocity_x *= -1
            if self.x <= ball_radius:
                self.x = ball_radius
            elif self.x >= width - ball_radius:
                self.x = width - ball_radius

        if self.y <= ball_radius:
            self.velocity_y *= -1
            self.y = ball_radius
        elif self.y >= height - ball_radius:
            self.velocity_y *= -1
            self.y = height - ball_radius

    def draw(self):
        pygame.draw.circle(screen, self.color, (self.x, self.y), ball_radius)

    def check_collision(self, other_ball):
        dx = self.x - other_ball.x
        dy = self.y - other_ball.y
        distance = math.sqrt(dx**2 + dy**2)

        # If the distance between centers is less than twice the radius, a collision occurred
        if distance < 2 * ball_radius:
            # Normalize the vector between the two balls
            normal_vector = pygame.math.Vector2(dx, dy).normalize()

            # Calculate relative velocity
            relative_velocity = pygame.math.Vector2(self.velocity_x, self.velocity_y) - pygame.math.Vector2(other_ball.velocity_x, other_ball.velocity_y)

            # Calculate the velocity along the normal vector (dot product)
            velocity_along_normal = relative_velocity.dot(normal_vector)

            if velocity_along_normal > 0:
                return

            # Calculate the impulse scalar
            impulse = 2 * velocity_along_normal / (2 * ball_radius)

            # Apply the impulse to the velocities of both balls
            self.velocity_x -= impulse * normal_vector.x
            self.velocity_y -= impulse * normal_vector.y
            other_ball.velocity_x += impulse * normal_vector.x
            other_ball.velocity_y += impulse * normal_vector.y

            # Reposition the balls to avoid overlap
            overlap = 2 * ball_radius - distance
            self.x += normal_vector.x * overlap / 2
            self.y += normal_vector.y * overlap / 2
            other_ball.x -= normal_vector.x * overlap / 2
            other_ball.y -= normal_vector.y * overlap / 2

# Create 3 balls of different colors
balls = [Ball(red), Ball(green), Ball(blue)]

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Move and check collisions for each ball
    for ball in balls:
        ball.move()

    # Check for collisions between the balls
    for i in range(len(balls)):
        for j in range(i + 1, len(balls)):
            balls[i].check_collision(balls[j])

    # Draw the window
    screen.fill(white)

    # Draw all balls
    for ball in balls:
        ball.draw()

    # Update and render
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
Enter fullscreen mode Exit fullscreen mode

Conclusion

This simple simulation demonstrates the basics of object-oriented programming in Python, along with an introduction to collision detection and physics simulations. With Pygame, you can create dynamic and interactive visualizations, making it an excellent choice for creating games and simulations.

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay