Chapter 10. The Project#

In this chapter we will apply the concepts we learned in the previous chapters and create a clone of the Breakout game. The goal of this game is to use a paddle to hit a ball and destroy bricks. You win, if all the bricks are destroyed and you lose if the ball touches the bottom wall:

We will use the pygame package designed for writing games in Python. You can install it using pip install --user pygame (no surprises there if you followed the last chapter).

The Game Loop#

We begin by writing the game loop. Here we create the screen object which we draw onto. We also create a clock that will allow us to control how often we want to render a frame (for this game we want to render 30 frames per second). We also define a while loop which repeatedly executes the following steps as long as the game is active:

  1. Process all currently existing user events. Right now we only process the pygame.QUIT event which happens if the user closes the window. If we see a pygame.QUIT event we set the active flag to False which will exit the game loop and therefore also exit the application (since there is no code following the game loop).

  2. Update the clock using the tick method.

  3. Repaint the screen using screen.fill and update it by calling pygame.display.flip. All game objects will be drawn between those two method calls.

Here is how the corresponding code looks like:

import pygame

w, h = 640, 480

pygame.init()
screen = pygame.display.set_mode((w, h))
pygame.display.set_caption("Breakout")

clock = pygame.time.Clock()

active = True
while active:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            active = False
            
        # more events will be processed here

    clock.tick(30)

    screen.fill((255, 255, 255))
    
    # game objects will be drawn here
    
    pygame.display.flip()

The Paddle#

Next we need to create our game objects. First we create a paddle that we can move around using our mouse.

The paddle needs to have the attributes x, y, w and h which denote its position and its dimensions. It also needs to have the usual __repr__ and __eq__ methods. Additionaly it should have (like all game objects) a render method which takes the screen object and renders the paddle onto the screen. Finally we need a move method which attempts to move the paddle to the respective mouse position.

Here is a first stab at the Paddle class:

class Paddle:
    def __init__(self, x, y, w, h):
        self.x = x
        self.y = y
        self.w = w
        self.h = h

    def move(self, mouse_pos):
        mouse_x, _ = mouse_pos
        self.x = int(mouse_x - self.w / 2)
        
    def render(self, screen):
        pygame.draw.rect(screen, (0, 127, 0), pygame.Rect(self.x, self.y, self.w, self.h))
        
    def __eq__(self, other):
        if not isinstance(other, Paddle):
            return False

        return self.x == other.x and self.y == other.y and self.w == other.w and self.h == other.h

    
    def __repr__(self):
        return f"Paddle(x={self.x}, y={self.y}, w={self.w}, h={self.h})"

Most of this is pretty self-explanatory except for the move method. We can’t move the paddle to the mouse position, because we want the paddle to always be anchored to the bottom of the screen. Therefore we ignore the y coordinate of the mouse position completely and only focus on the x coordinate. To make the movement feel right, we take the x coordinate of the mouse to be the center of the paddle.

This means that after the move mouse_x must be equal to self.x + self.w / 2 (since self.x indicates the left border of the paddle). Basic arithmetic therefore gives us the formula for the update: self.x = mouse_x - self.w / 2.

Let’s create a paddle object and play around with it:

paddle = Paddle(x=280, y=460, w=80, h=20)
paddle
Paddle(x=280, y=460, w=80, h=20)
paddle.move((240, 320))
paddle
Paddle(x=200, y=460, w=80, h=20)

This looks quite good! Now we need to add the paddle to our game. In order to accomplish this, two things need to be done. First, we need to check for pygame.MOUSEMOTION events and update the paddle position if such an event occurs. Second, we need to render the paddle in the game loop.

Here is the code we need to add:

paddle = Paddle(x=280, y=460, w=80, h=20)

...
while active:
    for event in pygame.event.get():
        ...

        if event.type == pygame.MOUSEMOTION:
            paddle.move(event.pos)

    ...

    clock.tick(30)
    
    screen.fill((255, 255, 255))
    paddle.draw(screen)
    pygame.display.flip()

    ...

Our game now looks like this:

paddle

Try moving the paddle around with your mouse and verify that the paddle moves along.

The Ball#

Next we need to add the ball. The ball should have a position x and y, a direction dx and dy and a radius r. The ball is not controlled by the player. Instead it simply moves on every frame by the amount given by dx and dy.

Here is how the Ball class looks like:

class Ball:
    def __init__(self, x, y, dx, dy, r):
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy
        self.r = r

    def move(self):
        self.x += self.dx
        self.y += self.dy

    def render(self, screen):
        pygame.draw.circle(screen, (0, 127, 0), (self.x, self.y), self.r)

    def __eq__(self, other):
        if not isinstance(other, Ball):
            return False
        return self.x == other.x and self.y == other.y and self.dx == other.dx and self.dy == other.dy and self.r == other.r

    def __repr__(self):
        return f"Ball(x={self.x}, y={self.y}, dx={self.dx}, dy={self.dy}, r={self.r})"

Again we have the special methods __eq__ and __repr__. We also have the render method which renders the ball onto the screen. Finally, we have a move method which updates the position of the ball.

Let’s create a ball object and have a look at its functionality:

ball = Ball(x=320, y=240, dx=-2, dy=2, r=10)
ball
Ball(x=320, y=240, dx=-2, dy=2, r=10)
ball.move()
ball
Ball(x=318, y=242, dx=-2, dy=2, r=10)

Nice work! Now we add a ball to the game loop. Since the ball doesn’t need to respond to external events, this is even simpler than the paddle. We create a ball object that starts at the center of the screen, call the move method after every tick and render the ball:

paddle = Paddle(x=280, y=460, w=80, h=20)
ball = ball = Ball(x=w // 2, y=h // 2, dx=-8, dy=8, r=10)
...

while active:
    ...
    
    clock.tick(30)
    
    ball.move()
    
    screen.fill((255, 255, 255))
    paddle.draw(screen)
    pygame.display.flip()

Our game now looks like this:

ball

If you run the game now, you will see a tiny problem - the ball moves through the bottom wall and disappears. This is because we currently have no collision detection. Since dy is negative, the ball just keeps moving downwards until it’s no longer inside the playing area.

The algorithm for collision detection will be relatively straightforward. We check if the ball collides with a wall and if that’s the case, we will simply reverse its direction. This leads to a problem. Finding out if the ball collides with one of the walls requires knowledge about the dimension of the screen.

We could pass that information along to the ball object, but that doesn’t seem right. Additionally, we will later need to coordinate multiple objects anyway (for example we will have to check for collisions between balls and bricks). In order to accomplish this, we create a Game class which will coordinate our game objects.

The Game Class#

Let’s think about the way the Game class should work. It needs to expose two methods - an update method that will update the game and a render method that will render the game. Here is how that might look like in code:

class Game:
    def __init__(self, w, h, paddle, ball):
        self.w = w
        self.h = h
        self.paddle = paddle
        self.ball = ball

    def update(self):
        self.ball.move()

    def render(self, screen):
        screen.fill((255, 255, 255))
        self.paddle.render(screen)
        self.ball.render(screen)
        pygame.display.flip()
        
    def __repr__(self):
        return f"Game(w={self.w}, h={self.h}, ball={self.ball}, paddle={self.paddle})"

Let’s play around with the Game class:

w, h = 640, 480
paddle = Paddle(x=280, y=460, w=80, h=20)
ball = Ball(x=w // 2, y=h // 2, dx=-8, dy=8, r=10)
game = Game(w, h, paddle, ball)
game
Game(w=640, h=480, ball=Ball(x=320, y=240, dx=-8, dy=8, r=10), paddle=Paddle(x=280, y=460, w=80, h=20))
game.update()
game
Game(w=640, h=480, ball=Ball(x=312, y=248, dx=-8, dy=8, r=10), paddle=Paddle(x=280, y=460, w=80, h=20))
game.paddle.move((240, 320))
game
Game(w=640, h=480, ball=Ball(x=312, y=248, dx=-8, dy=8, r=10), paddle=Paddle(x=200, y=460, w=80, h=20))

This looks good. Now we update the game loop. Instead of updating and rerendering all game objects separately, the game object will now take care of updates and renders:

clock = pygame.time.Clock()

paddle = Paddle(x=280, y=460, w=80, h=20)
ball = Ball(x=w // 2, y=h // 2, dx=-8, dy=8, r=10)
game = Game(w, h, paddle, ball, 8, 6)

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

        if event.type == pygame.MOUSEMOTION:
            paddle.move(event.pos)

    clock.tick(30)

    game.update()
    game.render(screen)

Much more readable. In addition, we will practically never have to change the game loop again. If we want to add new objects to game, all we need to do is to update the Game class.

Note that no functionality has changed. We just refactored our code - we spent some time cleaning up and improving the structure in order to make it easier to make changes later on. Regular refactoring is an absolutely vital part of software development - otherwise you will soon find yourself with a mess of utterly unreadable spaghetti code.

Let’s refactor some more and split the different pieces into different modules.

Splitting the Project into Modules#

Create a file paddle.py and move the Paddle class into that file. Here is how paddle.py should look like now:

import pygame


class Paddle:
    def __init__(self, x, y, w, h):
        self.x = x
        self.y = y
        self.w = w
        self.h = h

    def move(self, mouse_pos):
        mouse_x, _ = mouse_pos
        self.x = int(mouse_x - self.w / 2)

    def render(self, screen):
        pygame.draw.rect(screen, (0, 127, 0), pygame.Rect(self.x, self.y, self.w, self.h))

    def __eq__(self, other):
        if not isinstance(other, Paddle):
            return False

        return self.x == other.x and self.y == other.y and self.w == other.w and self.h == other.h

    def __repr__(self):
        return f"Paddle(x={self.x}, y={self.y}, w={self.w}, h={self.h})"

Next create a file ball.py and move the Ball class into that file. Here is how ball.py should look like now:

import pygame


class Ball:
    def __init__(self, x, y, dx, dy, r):
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy
        self.r = r

    def move(self):
        self.x += self.dx
        self.y += self.dy

    def render(self, screen):
        pygame.draw.circle(screen, (0, 127, 0), (self.x, self.y), self.r)

    def __eq__(self, other):
        if not isinstance(other, Ball):
            return False
        return self.x == other.x and self.y == other.y and self.dx == other.dx and self.dy == other.dy and self.r == other.r

    def __repr__(self):
        return f"Ball(x={self.x}, y={self.y}, dx={self.dx}, dy={self.dy}, r={self.r})"

We also move the Game into a file game.py:

class Game:
    def __init__(self, w, h, paddle, ball):
        self.w = w
        self.h = h
        self.paddle = paddle
        self.ball = ball

    def update(self):
        self.ball.move()

    def render(self, screen):
        screen.fill((255, 255, 255))
        self.paddle.render(screen)
        self.ball.render(screen)
        pygame.display.flip()
        
    def __repr__(self):
        return f"Game(w={self.w}, h={self.h}, ball={self.ball}, paddle={self.paddle})"

Finally let’s move the game loop to a file breakout.py:

import pygame

from ball import Ball
from game import Game
from paddle import Paddle

w, h = 640, 480

pygame.init()
screen = pygame.display.set_mode((w, h))
pygame.display.set_caption("Breakout")

clock = pygame.time.Clock()

paddle = Paddle(x=280, y=460, w=80, h=20)
ball = Ball(x=w // 2, y=h // 2, dx=-8, dy=8, r=10)
game = Game(w, h, paddle, ball, 8, 6)

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

        if event.type == pygame.MOUSEMOTION:
            paddle.move(event.pos)

    clock.tick(30)

    game.update()
    game.render(screen)

Collision Detection#

Now that we have refactored our code, adding collisions with the paddle and the wall is pretty easy.

Let’s write a helper function that checks if a rectangle collides with a circle. We will keep it really simple and just check whether the center of the circle is inside the rectangle. Add that function to game.py:

def coords_in_rect(x, y, rect_x, rect_y, rect_w, rect_h):
    return rect_x < x < rect_x + rect_w and rect_y < y < rect_y + rect_h

Now we update the Game class:

class Game:
    ...

    def update(self):
        self.ball.move()

        # If the ball collides with the paddle, we reverse its direction
        if coords_in_rect(self.ball.x, self.ball.y, self.paddle.x, self.paddle.y, self.paddle.w, self.paddle.h):
            self.ball.dy *= -1

We can now hit the ball with the paddle, but the ball still disappears through the wall. We need to check for wall collisions as well:

class Game:
    ...

    def update(self):
        self.ball.move()

        # If the ball collides with the paddle, we reverse its direction
        if coords_in_rect(self.ball.x, self.ball.y, self.paddle.x, self.paddle.y, self.paddle.w, self.paddle.h):
            self.ball.dy *= -1
            
        # Check for wall collisions
        if self.ball.x - self.ball.r < 0 or self.ball.x + self.ball.r > self.w:
            self.ball.dx *= -1

        if self.ball.y - self.ball.r < 0 or self.ball.y + self.ball.r > self.h:
            self.ball.dy *= -1

Adding the Bricks#

We now have a ball that bounces around and can be hit with a paddle. However, we are still missing the actual game, i.e. the possibility to hit some bricks with our ball.

Let’s create a file brick.py containing the Brick class:

class Brick:
    def __init__(self, x, y, w, h):
        self.x = x
        self.y = y
        self.w = w
        self.h = h

    def render(self, screen):
        pygame.draw.rect(screen, (0, 127, 0), pygame.Rect(self.x, self.y, self.w, self.h))
        
    def __eq__(self, other):
        if not isinstance(other, Brick):
            return False

        return self.x == other.x and self.y == other.y and self.w == other.w and self.h == other.h
    
    def __repr__(self):
        return f"Brick(x={self.x}, y={self.y}, w={self.w}, h={self.h})"

We will also add some helper functions that create a list of bricks for our game in brick.py:

def get_brick_w(w, n_bricks_x):
    return 2 * w / (3 * n_bricks_x + 1)


def get_brick_h(h, n_bricks_y):
    return h / (3 * n_bricks_y + 1)


def get_bricks(n_bricks_x, n_bricks_y, brick_w, brick_h):
    bricks = []
    for i in range(0, n_bricks_x):
        for j in range(0, n_bricks_y):
            brick = Brick(
                x=i * brick_w + (i + 1) * brick_w / 2,
                y=j * brick_h + (j + 1) * brick_h / 2,
                w=brick_w,
                h=brick_h,
            )
            bricks.append(brick)
    return bricks

Now we add the bricks to the Game class:

...
from brick import get_brick_w, get_brick_h, get_bricks
...

class Game:
    def __init__(self, w, h, ball, paddle, n_bricks_x, n_bricks_y):
        ...

        self.n_bricks_x = n_bricks_x
        self.n_bricks_y = n_bricks_y

        brick_w = get_brick_w(w, n_bricks_x)
        brick_h = get_brick_h(h, n_bricks_y)
        self.bricks = get_bricks(n_bricks_x, n_bricks_y, brick_w, brick_h)

Don’t forget to update the game object in breakout.py:

game = Game(w, h, paddle, ball, 8, 6)

Rendering the bricks is pretty simple. We just iterate through all the bricks in the render method and render each brick:

class Game:
    ...
    
    def render(self, screen):
        screen.fill((255, 255, 255))
        self.paddle.render(screen)
        self.ball.render(screen)
        
        # Render the bricks
        for brick in self.bricks:
            brick.render(screen)

        pygame.display.flip()

We also destroy the bricks that are hit by the ball. If at least one brick is hit, we reverse the balls direction:

class Game:
    ...
    
    def update(self):
        ...
        
        prev_len = len(self.bricks)
        self.bricks = [brick for brick in self.bricks if not coords_in_rect(self.ball.x, self.ball.y, brick.x, brick.y, brick.w, brick.h)]
        if len(self.bricks) < prev_len:
            self.ball.dy *= -1

This looks pretty neat:

Ending the Game#

We are pretty much done, all that is left to do is end the game when the player wins or loses. To accomplish that we will maintain a game state which will be one of "ongoing", "won" or "lost" and render different things depending on the game state.

First, we update the Game class:

class Game:
    def __init__(self, w, h, paddle, ball, n_bricks_x, n_bricks_y):
        ...

        self.state = "ongoing"
        self.font = pygame.font.SysFont("Arial", 30)

    def update(self):
        ...

        if self.ball.y + self.ball.r > self.h:
            self.state = "lost"

        ...

        if len(self.bricks) == 0:
            self.state = "won"

Next we update the render method of the Game class:

class Game:
    ...
    
    def render(self, screen):
        if self.state == "ongoing":
            screen.fill((255, 255, 255))
            self.paddle.render(screen)
            self.ball.render(screen)
            for brick in self.bricks:
                brick.render(screen)
            pygame.display.flip()
        else:
            screen.fill((255, 255, 255))
            surface = self.font.render(f"You {self.state}", False, (0, 127, 0))
            text_rect = surface.get_rect(center=(self.w / 2, self.h / 2))
            screen.blit(surface, text_rect)
            pygame.display.flip()
     
    ...

Finally we need to call pygame.font.init() in breakout.py:

pygame.init()
pygame.font.init()
screen = pygame.display.set_mode((w, h))

Now if we destroy all bricks we win the game:

Similarly if the ball disappers throught the bottom of the screen we lose the game.

Wrapping Up#

Congratulations, you just completed your first game! From here, there are a number of things you can try by yourself:

  1. The paddle currently partially disappears, if we move the mouse too far left or too far right. This can be improved by checking whether there is a collision between the paddle and a wall.

  2. Try adding a score counter to the game.

  3. Improve the collision detection between the ball and the various game elements.

  4. Add a timer to the game, so that the player only has a limited amount of time to destroy all the bricks!

  5. Improve the user experience. The game looks quite flat and boring. This could be fixed by playing around with colors, shadows etc.

  6. Allow the player to restart the game by pressing a special key after he won or lost the game.

And many more… The only limit is your imagination!