How to Build a Breakout Game with PyGame in Python

Learn to build a classic Breakout game in Python using Pygame. This tutorial covers setting up the game window, creating the paddle, ball, and bricks, adding ball bouncing logic, updating scores, and incorporating sounds.
  · 19 min read · Updated jun 2024 · Game Development

Struggling with multiple programming languages? No worries. Our Code Converter has got you covered. Give it a go!

Programming involves your ability to manipulate code effectively. This is the quality that allows you to build worthy and thought provoking applications.

Building game projects is one of the best ways to learn code manipulation. In Python, a popular library used for building interactive games is Pygame

In this tutorial, you will learn how to build a Breakout game in Python. It is a classic game featuring a movable paddle, a bouncing ball, and multiple overhead bricks.

The objective of this game is to use the ball to destroy all the bricks while using the paddle to control the ball. You can check out a demo of the game below:

Demo video on YouTube

By the end of this tutorial, you will learn how to build a fully interactive game using Pygame and manipulate the code to meet your specific needs.

Table of Contents:

Requirements

To follow along with this tutorial, you need the following:

1. Pygame: Run the command below to install pygame.

pip install pygame 

2. Freesound account: We will need to add sounds when certain events occur in our game. Freesound has a library of free soundtracks from which you can get game sounds. You need to create an account to download the sounds.

Configure the Game Settings

The first step is to create a settings.py file in the project folder. This file will contain all the settings and variables for the game. Below is the content of the settings.py file:

# settings.py
import pygame as pg

# Screen settings
WIDTH = 550
HEIGHT = 600
BG_COLOR = "purple"
# Text color
color = "white"
# Paddle settings
paddle_x = 250
paddle_y = 550
paddle_width = 100
paddle_height = 20
# Ball settings
ball_x = 250
ball_y = 540
ball_x_speed = 2
ball_y_speed = 2
ball_radius = 5
ball_color = "grey"
# Text settings
text_x = 300
# Bricks settings
brick_width = 40
brick_height = 20

As stated earlier, a breakout game contains a paddle, ball, set of bricks, and a scoreboard to keep track of scores. Therefore, the settings above will be used to set up the size, dimension, position (x, y), and other features of each object in the game.

Creating the Game Window

We must set up the game screen by specifying a suitable dimension, color, etc. Create a new file named main.py. This file will serve as the main script for the game. Add the code below to this file:

# main.py
import pygame as pg
from settings import *

# Initialize pygame
pg.init()

screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("Breakout Game")

clock = pg.time.Clock() 

running = True
while running:
   screen.fill(BG_COLOR)
   # Check for quit game
   for event in pg.event.get():
       if event.type == pg.QUIT:
           running = False
   pg.display.flip()
   # To adjust the game frame rate
   clock.tick(60)

The code above is like a boilerplate for setting up a game window in pygame. We first initialized pygame and set the window dimension already using the variables in the settings.py file. 

The Clock() class is used to manage the frame rate per second. This ensures the game does not run too fast and is consistent on all systems. To keep the window open, we need to create a while loop where every event in the game will be added. 

Finally, we set up a loop that quits the game when a user closes the game window. The pg.display.flip() method ensures every object and event that occurs in the game is updated regularly.

Creating the Paddle

The screen is currently blank, so we need to add objects. The first object we will add is the paddle. To add the paddle, you need to create a new python file named paddle.py. It is in this file that we will create our Paddle class.

Since our project requires us to add objects such as paddles, balls, bricks, etc. It is recommended that we split each one of them into separate classes.

Add the following code to the paddle.py file:

# paddle.py
import pygame as pg
from settings import paddle_height, paddle_width

class Paddle:
   def __init__(self, x, y):
       self.x = x
       self.y = y
       self.width = paddle_width
       self.height = paddle_height
       self.rect = pg.Rect(self.x, self.y, self.width, self.height)
       self.color = pg.Color("white")

   def appear(self, screen):
       pg.draw.rect(screen, self.color, self.rect)

   def move_right(self):
       if self.rect.x + self.width <= 550:
           self.rect.x += 2

   def move_left(self):
       if self.rect.x >= 0:
           self.rect.x -= 2

In the code above, we created a Paddle class and set its properties, such as the x and y positions on the screen, height, width, and color. The paddle is created from a pygame rectangle object, which is represented by the self.rect attribute.

The appear() method displays the paddle on the screen. 

The other two methods - move_right() and move_left() add motion to the Paddle by moving it to the right and left by 2 steps, respectively. These methods also ensure that the paddle only moves within the screen dimensions. Therefore, if you try to move the paddle beyond the screen, it will not move.

We need to add the paddle object to the main script. Open the main.py file and do the following:

1. Import the paddle class and create a paddle object

from paddle import Paddle

...
# OBJECTS
pad = Paddle(paddle_x, paddle_y)

...

2. Add the appear() method to the while loop like below:

while running:

   ...   
   pad.appear(screen)
   ...

3. Check for key presses to move the ball to either the right or left:

while running:

   ...   
   
   # Check for key presses
   keys = pg.key.get_pressed()
   if keys[pg.K_RIGHT]:
       pad.move_right()

   if keys[pg.K_LEFT]:
       pad.move_left()   
   ...

Your main.py file should now look like the one below:

# main.py

import pygame as pg
from paddle import Paddle
from settings import *

pg.init()

screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("Breakout Game")
clock = pg.time.Clock()
# OBJECTS
pad = Paddle(paddle_x, paddle_y)

running = True
while running:
   screen.fill(BG_COLOR)
   pad.appear(screen)
   # Check for quit game
   for event in pg.event.get():
       if event.type == pg.QUIT:
           running = False
   # Check for key presses
   keys = pg.key.get_pressed()
   if keys[pg.K_RIGHT]:
       pad.move_right()
   if keys[pg.K_LEFT]:
       pad.move_left()
   pg.display.flip()
   clock.tick(60)

Run the program. You should see a paddle appear on the screen like the one below:

Use the right and left arrow keys to control the paddle.

Building the Bouncing Ball

Just as we did for the paddle, create a new file named ball.py and create a Ball class. Add the code below to this file:

# ball.py
import pygame as pg
from settings import ball_x_speed, ball_y_speed, ball_radius, ball_color

class Ball:
   def __init__(self, x, y, screen):
       self.x = x
       self.y = y
       self.screen = screen
       self.radius = ball_radius
       self.color = pg.Color(ball_color)
       self.x_speed = ball_x_speed
       self.y_speed = ball_y_speed

   def move(self):
       pg.draw.circle(self.screen, self.color, [self.x, self.y], self.radius)
       self.y -= self.y_speed
       self.x -= self.x_speed

In the code above, we created a class for the ball and set its properties. The move() function draws the ball at the specified coordinate on the screen and adds motion to it. It achieves this by reducing the x and y values of the ball. 

Follow the steps below to add the ball to the screen:

1. Open the main.py file 
2. Import the Ball class and create a ball object:

...
from ball import Ball
...

# OBJECTS
ball = Ball(ball_x, ball_y, screen)

3. Add the .move() method to the while loop:

running = True
while running:
   ...
   ball.move()
   ...

This will ensure the ball’s position is regularly updated on the screen.

Adding the Ball Bouncing Logic

The ball just keeps rolling off the screen. Originally, it was supposed to bounce anytime it hit the screen or the paddle edges  (except for the bottom part of the screen). We need to add new methods to the Ball() class to implement this. Below are the methods:

...

def bounce_x(self):
   self.x_speed *= -1

def bounce_y(self):
   self.y_speed *= -1

def check_for_contact_on_x(self):
   if self.x - self.radius <= 0 or self.x + self.radius >= self.screen.get_width():
       self.bounce_x()

def check_for_contact_on_y(self):
   if self.y + self.radius <= 0:
       self.bounce_y()

The bounce_x() and bounce_y() methods implement the ball-bouncing logic for the x and y axis respectively.

The check_for_contact_on_y() and check_for_contact_on_x() methods compare the ball’s distance with the screen edges using its radius and current x and y positions.

These methods create a reverse of the ball's current position by multiplying it by -1. For example, if the ball is moving upward, then it hits the screen on the right, we need to make it bounce back to the left by reducing its x and y value. The only way to do this is by multiplying it by -1, so it becomes negative and starts to move towards the left side.

Similarly, if the ball is moving upward and it hits the screen on the left, we need to make it bounce to the right by increasing its x and y value. 

Since, x is negative already, the only way to make it positive is by multiplying it by -1, so it starts to move towards the right side.

Let’s reflect these changes in the while loop as seen below:

running = True
while running:
   ...
   ball.move()
   ...
   # Check if ball hits the x-axis above 
   ball.check_for_contact_on_x()
   # Check if ball hits y-axis on the side
   ball.check_for_contact_on_y()
   # Check if ball hits paddle
   if (pad.rect.y < ball.y + ball.radius < pad.rect.y + pad.height
          and
       pad.rect.x < ball.x + ball.radius < pad.rect.x + pad.width):
       ball.bounce_y()
       ball.y = pad.y - ball.radius
   ...

The ball will bounce when it hits the screen edges or the paddle.

Adding Bricks to the Game

The bricks in a breakout game are always above the paddle. They usually appear in different colors. We will need to do some mathematics to add the bricks correctly. Don't fret; it's not rocket science. I'll show you how to go about it.

Create a new Python file named bricks.py and add a class named Bricks:

# bricks.py
import random
import pygame as pg

class Bricks:
   def __init__(self, screen, width, height):
       self.screen = screen
       self.width = width
       self.height = height
       self.random_colors = ['blue', 'yellow', 'red', 'green', 'orange']
       self.bricks = []
       self.brick_colors = []
       self.set_values()

   def set_values(self):
       y_values = [int(y) for y in range(100, 200, 25)]
       x_values = [int(x) for x in range(10, 550, 42)]
       y_index = 0
       self.loop(x_values, y_values, y_index)

   def loop(self, x_values, y_values, y_index):
       for n in x_values:
           # Check if it is the last position in the x_values list.
           if n == x_values[-1]:
               # Check if all the positions in the y_values has been occupied
               if y_index < len(y_values) - 1:
                   y_index += 1
                   # Run the method again if there are still vacant positions.
                   self.loop(x_values, y_values, y_index)
           # Create new bricks
           else:
               x = n
               y = y_values[y_index]
               brick = pg.Rect(x, y, self.width, self.height)
               self.bricks.append(brick)
               self.brick_colors.append(random.choice(self.random_colors))

   def show_bricks(self):
       for loop in range(len(self.bricks)):
           brick = self.bricks[loop]
           color = self.brick_colors[loop]
           pg.draw.rect(self.screen, color, brick)

In the Bricks class, we added the brick properties and other attributes, which include the following:

  • self.random_colors: This is a list of colors that the bricks can pick from
  • self.bricks: This is a list that will contain all the bricks generated for the game 
  • self.brick_colors: This list will contain the already selected colors for each brick created in the game.

The set_values() method creates a list of values for the x and y-axis. The y_index will be used to access each value for the y-axis in the list. But for now, it is 0.

The loop() method takes three positional arguments. They are the variables we added to the set_values() method earlier.

Now, what does the code in the loop() method do?

We already have a list of x and y positions for our bricks. All that is needed is to place them on each point. One way to do this is by selecting a position on the y-axis, and arrange the bricks along this position, with each brick having an x-value from the list of generated positions for x.

The y_index increases by 1 when a particular y-axis is filled up. This way, it moves to the next y-axis on the next loop. This continues until all the y-axis are occupied. Each brick is then added to the self.bricks list.

Finally, the show_bricks() method displays the bricks on the screen by looping through the self.bricks list and give each brick a color from the self.brick_colors list.

Now, let’s add this to the game loop. Follow the steps below to do this:

1. Import the Bricks class and create a brick object

...
from bricks import Bricks
...
# OBJECTS
bricks = Bricks(screen, brick_width, brick_height)

...

2. Add the show_bricks() method to the while loop like below:

running = True
while running:
   ...
   bricks.show_bricks()
   ...

Run the program, and you should see colorful bricks arranged on the screen like in the image below:

Checking When the Ball Hits the Brick

According to the game rules, the bricks break and disappear once the ball hits them. We can implement this by checking if the ball has collided with the brick. Add a new if block to the while loop to add this feature:

...
running = True
while running:
   ...
   # Check if ball hits brick
   for brick in bricks.bricks:
       if brick.collidepoint(ball.x, ball.y - ball.radius) or brick.collidepoint(ball.x, ball.y + ball.radius):
           bricks.bricks.remove(brick)
           ball.bounce_y()
   ...

In the code above, a brick is removed from the list when it gets hit, and the ball bounces back.

Updating Scores

The next step is to record your scores for each successful hit and then reduce your trials/lives when the paddle misses the ball.

Create a new file named scores.py. In this file, create a Scoreboard class that will handle the scores. Below is the ScoreBoard class:

# scores.py

import pygame as pg

class ScoreBoard:
   def __init__(self, x, color, screen):
       self.screen = screen
       self.color = color
       self.x = x
       self.score = 0
       self.high_score = 0
       self.trials = 2
       self.font = pg.font.SysFont("calibri", 20)

   def show_scores(self):
       score_text = self.font.render(f"Score: {self.score}", True, self.color)
       high_score_text = self.font.render(f"High Score: {self.high_score}", True, self.color)
       trials_text = self.font.render(f"Trials: X{self.trials}", True, self.color)

       score_text_rect = score_text.get_rect(topleft=(self.x, 10))
       high_score_text_rect = high_score_text.get_rect(topleft=(self.x, 26))
       trials_text_rect = trials_text.get_rect(topleft=(self.x, 42))

       self.screen.blit(score_text, (self.x, 10))
       self.screen.blit(high_score_text, (self.x, 26))
       self.screen.blit(trials_text, (self.x, 42))

   def is_game_over(self):
       if self.trials == 0:
           return True
       return False

   def game_over(self):
       game_over_color = 'red'
       game_over_font = pg.font.SysFont("calibri", 30)
       game_over_text = game_over_font.render(f"Game Over! Click '0' to restart.", True, game_over_color)
       game_over_rect = game_over_text.get_rect(topright=(50, 300))
       self.screen.blit(game_over_text, (50, 300))
       self.record_high_score()

   def success(self):
       game_success_color = 'green'
       game_success_font = pg.font.SysFont("calibri", 30)
       game_success_text = game_success_font.render(f"You won! Click '0' to restart.", True, game_success_color)
       game_success_rect = game_success_text.get_rect(topleft=(50, 300))
       self.screen.blit(game_success_text, (50, 300))
       self.record_high_score()

   def set_high_score(self):
       try:
           with open("records.txt", mode="r") as file:
               lines = file.readlines()
       except FileNotFoundError:
           with open("records.txt", mode="w") as data:
               data.write("0")
               score = 0
       else:
           score = lines[0]

       self.high_score = int(score)

   def record_high_score(self):
       if self.score > self.high_score:
           with open("records.txt", mode="w") as file:
               file.write(f"{self.score}")

In this code, we set the scoreboard properties like font, height, etc.. The show_scores() method displays the scores, high scores, and trials on the screen. 

As in previous examples, import the Scores class and create a new score object. Add the code below to the while loop:

...
running = True
while running:
   ...
   # Check if ball hits brick
   for brick in bricks.bricks:
       if brick.collidepoint(ball.x, ball.y - ball.radius) or brick.collidepoint(ball.x, ball.y + ball.radius):
           bricks.bricks.remove(brick)
           ball.bounce_y()
           # Increase scores by 1
          score.score += 1
    # Check if ball falls off
    if ball.y + ball.radius >= 580:
          ball.y = pad.y - ball.radius
          pg.time.delay(2000)
          score.trials -= 1
          ball.bounce_y()
...

This way, if the ball hits a brick, your score increases by 1.  Also, your score will be reduced if the ball drops beyond the paddle. Below is an image showing the scoreboard:

Checking Game over Condition

The game ends when no trials are left or all the bricks have been destroyed. We must create a condition that ends the game when the number of trials or number of bricks equals 0 .

In the ScoreBoard class, there are three methods namely game_over()is_game_over() and success()

The is_game_over() and game_over() methods will be called when a user has no trials left, that is, he/she has lost, while the success() method is called when a user breaks all the bricks. Each method displays a body of text describing each event.

The last two methods set_high_score() and  record_high_score() add continuity to this game. For now, you can only play for the moment and your scores are not recorded. These methods change this by keeping a record of your highest score in a .txt document. This is why they are added to the success() and  game_over() methods, so the high score will be updated when the game ends

Therefore, when you restart the game, your highest score will appear on the scoreboard. Let’s update the main script to contain the new updates. Add the code below to the main.py file:

...
# OBJECTS
...
score = ScoreBoard(text_x, color, screen)
score.set_high_score

running = True
while running:
   ...
   # Check if there are more trials
   if score.is_game_over():
       score.game_over()
   # Check if all bricks are broken
   elif len(bricks.bricks) == 0:
       score.success()
   else:
       ball.move()
   ...

In the code above, we update the while loop so that if the remaining trial equals 0 or the user breaks all the bricks, the ball should stop moving. This is why the ball.move() method is moved to an if block. Therefore, the ball stops moving when any of the conditions is met.

Below is an image showing when a user lost:

Below is an image showing when a user wins:

Adding Sounds to the Game

The next step is to add sounds to the game. We will be adding sounds for certain events in the game

As stated at the beginning of this tutorial, Freesound.org has a vast library of background sounds to use. I have selected the audio files that will be used for this tutorial. However, you are at liberty to choose anyone of your choice. Below is a list of the audio files that will be used in the game and where to use them:

An audio file format that works best for pygame is .ogg. It packages audio files into small sizes, which ensures delivery speed. You can use a free online audio converter to convert the audio files to .ogg.

After converting these files, then you can use it for the game via the steps below:

  1. Create a separate folder in your project named audio
  2. Add the audio files to this folder.
  3. Open the settings.py file and initialize the sounds in Pygame
# settings.py
...
# Initialize Sound
pg.mixer.init()
# Audio files
pad_hit = pg.mixer.Sound('audio/pad_hit.ogg')
brick_breaking = pg.mixer.Sound("audio/brick_breaking.ogg")
game_end = pg.mixer.Sound("audio/game_end_.ogg")
dropping_ball = pg.mixer.Sound("audio/dropping_ball.ogg")
win_game = pg.mixer.Sound("audio/win_game.ogg")
...

The code above initializes the audio sounds for the project. Open the main.py file and add sounds to the specific events as seen below:

# main.py
...
sound_played = False
running = True
while running:
    ...
    # Check if there are more trials
    if score.is_game_over():
       if not sound_played:
          # Sound added
          pg.mixer.Sound.play(game_end)
          sound_played = True
          score.game_over()
    # Check if all bricks are broken
    elif len(bricks.bricks) <= 0:
       if not sound_played:
          # Sound added
          pg.mixer.Sound.play(win_game)
          sound_played = True
          score.success()
    else:
         ball.move()
    ...
    # Check if ball falls off
    if ball.y + ball.radius >= 580:
       # Sound added
       pg.mixer.Sound.play(dropping_ball)
       ball.y = pad.y - ball.radius
       pg.time.delay(2000)
       score.trials -= 1
       ball.bounce_y()
    # Check if ball hits paddle
    if (pad.rect.y < ball.y + ball.radius < pad.rect.y + pad.height
           and
        pad.rect.x < ball.x + ball.radius < pad.rect.x + pad.width):
       # Sound added  
       pg.mixer.Sound.play(pad_hit)
       ball.bounce_y()
       ball.y = pad.y - ball.radius
    # Check if ball hits brick
    for brick in bricks.bricks:
       if brick.collidepoint(ball.x, ball.y - ball.radius) or        brick.collidepoint(ball.x, ball.y + ball.radius):
          # Sound added
          pg.mixer.Sound.play(brick_breaking)
          bricks.bricks.remove(brick)
          ball.bounce_y()
          score.score += 1
    ...

Adding a Restart Key

The final step in this game is allowing users to restart it when they lose or win. We need to add a key listener event to restart the game when the user clicks "0". Here is the code below:

...
sound_played = False
running = True
while running:
   ...
   # Check for key presses
   ...
   # Restart game
   if keys[pg.K_0]:
       if score.is_game_over():
           score.score = 0
           score.trials = 5
           score.sound_played = False
           bricks.bricks.clear()
           bricks.set_values()
   ...

This code resets all the game variables to the default settings. 

Conclusion

This project is an awesome way to start building games in Python. You have learned how to build a fully-fledged desktop game with the Pygame library. We were also able to add sounds to make the game more entertaining.

You can now confidently take up similar projects to build by yourself. Let me know what you think about this game, and other questions you may have for me in the comments section.

You can view the full code here.

Related tutorials:

Until next time, Pythonista 🐍

Just finished the article? Why not take your Python skills a notch higher with our Python Code Assistant? Check it out!

View Full Code Understand My Code
Sharing is caring!



Read Also



Comment panel

    Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!