Introduction

In our previous articles, we created the Pong and Snake games in Python using Pygame. However, those games included only the basics of pygame, we had just drawn some shapes like rectangles and circles.

This time we a go a bit further — we'll create a breakout (or arkanoid) game using sprites.

As always, if you're interested only in the source code, feel free to jump straight to the Summary section.

breakout game in python using pygame and sprites

What are Sprites?

In computer graphics sprite basically means a bitmap. A bitmap, that represents a graphical object (like a brick, paddle or ball in our case) on the screen. It can be either animated or static. For example, in the legendary Super Mario game, Mario's character is a sprite.

Pygame provides an excellent way of working with sprites such as:

  • Loading sprites from image files
  • Collision detection
  • We can also specify the type of the collision detecting
  • Combining sprites into groups and allowing us to detect collisions between two groups

The full documentation is available here.

The Game Loop

Like any other game, the simplified version of any game's game loop is as follows:

  1. Initialize
  2. Process events (keyboard, mouse, controller, etc.)
  3. Update (like moving the player's position)
  4. Render

Let's start with the following game loop in our breakout game:

import os
import pygame

os.environ['SDL_VIDEO_WINDOW_POS'] = "200,30"

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

GRAY = (100, 100, 100)

def main():
    pygame.init()

    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption('Breakout Game | codingboost.com')
    clock = pygame.time.Clock()

    done = False
    while not done:
        screen.fill(GRAY)

        keys = pygame.key.get_pressed()

        if keys[pygame.K_ESCAPE]:
            done = True

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                done = True

        pygame.display.update()
        clock.tick(60)

main()

This doesn't do that much yet, just shows an empty gray screen.

The only "non-standard" thing in this game loop code is the inclusion of the os module. I needed this because I wanted to fix the position of the window. It can be done by passing the x and y coordinates to the SDL_VIDEO_WINDOW_POS environment variable.

It's also worth mentioning that we set a framerate-cap to 60 FPS with the clock.tick(60) instruction.

Adding the Sprites to the Game

The breakout game consists of three graphical objects:

  • Brick
  • Ball
  • Paddle

Let's start with the brick. We'll create a new class to represent the brick sprite in the game:

GRAY = (100, 100, 100)
WHITE = (255, 255, 255)

class Brick(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((50, 20))
        self.image.fill(WHITE)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

To create a sprite, we need to extend the pygame.sprite.Sprite. Also, according to the documentation we have to invoke the Sprite class' constructor too.

The self.image attribute describes the image we'll display for this sprite. For the time being, we just set a 50x20 white surface.

The position of the sprite will be updated according to the sprite object's rect attribute. The dimension of the rectangle will be the same as the previously created image. The position itself is controlled by the x and y variables respectively.

You may ask why do we need to provide a rectangle? Why just don't provide the x and y values for the position? The answer is simple: collision detection. One of the possible collision detection mechanism is based on rectangles. That being said, collision of sprites is tested against their respective rectangles.

Once we've defined our brick sprite, let's start using it. Add this piece of code before the loop inside the main function:

    brick_list = pygame.sprite.Group()
    for i in range(0, 10):
        for j in range(0, 3):
            brick = Brick(70 + i * 70, 20 + j * 50)
            brick_list.add(brick)

The first instruction in the code above creates a group of brick sprites. That's obvious since a breakout game includes many bricks that we want to destroy.

The second part of the code is just creating many bricks and adding it to the brick_list sprite group.

Then we're ready to draw the sprites:

# ...
brick_list.update()
brick_list.draw(screen)
pygame.display.update()
clock.tick(60)
# ...

If everything goes well, we'll see the following screen:

Displaying bricks.

Loading Image Files for Sprites

Using white rectangles may be a bit boring. Let's replace them with some cool image! The image attribute looks like this so far:

self.image = pygame.Surface((50, 20))
self.image.fill(WHITE)

However, instead of using pygame.Surface, we can use an actual image loaded from a file for our sprite. Replace the two lines to the following:

# self.image = pygame.Surface((50, 20))
# self.image.fill(WHITE)
self.image = pygame.image.load('brick.png')

I used the sprites downloaded here.

After applying this change let's run the game again:

Displaying sprites now using assets/.

That rocks! 🔥🔥

Adding the remaining sprites - Paddle and Ball

Based on the example above let's add the missing pieces of the puzzle. Start with the paddle:

class Paddle(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load('paddle.png')
        self.rect = self.image.get_rect()
        self.rect.center = (x, y)

    def move_left(self):
        self.rect.x -= 20

    def move_right(self):
        self.rect.x += 20

The only difference compared to the brick is that the paddle is not a static object, so we need to move. Therefore we defined two methods (move_left and move_right) to change the paddle's position respectively. These methods will be called from the event loop.

Then we continue with creating the Ball class:

class Ball(pygame.sprite.Sprite):
    BALL_SPEED = 8
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.direction_x = 1
        self.direciton_y = 1
        self.image = pygame.image.load('ball.png')
        self.mask = pygame.mask.from_surface(self.image)
        self.rect = self.image.get_rect()
        self.rect.center = (x, y)

    def flip_direction_x(self):
        self.direction_x *= -1

    def flip_direction_y(self):
        self.direciton_y *= -1

    def leaves_screen_bottom(self):
        if self.rect.x < 0 or self.rect.x > SCREEN_WIDTH:
            self.flip_direction_x()
        if self.rect.y < 0:
            self.flip_direction_y()

        return self.rect.y > SCREEN_HEIGHT

    def move(self):
        self.rect.x += self.BALL_SPEED * self.direction_x
        self.rect.y += self.BALL_SPEED * self.direciton_y

The flip_direction methods just do what they claim to do: They change the balls x or y direction to the opposite.

Then we need a control method leaves_screen_bottom that checks whether the ball has left the screen. That event could trigger a Game Over screen.

The ball's position is changed in the move method.

Now let's add the sprites to the sprites group (before the game loop):

paddle = Paddle(100, 550)
ball = Ball(250, 250)

brick_list = pygame.sprite.Group()
paddle_list = pygame.sprite.Group()
ball_list = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()

paddle_list.add(paddle)
ball_list.add(ball)
all_sprites.add(paddle)
all_sprites.add(ball)

And start watching the keyboard events for the left and right characters.

keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
    paddle.move_left()

if keys[pygame.K_RIGHT]:
    paddle.move_right()

if keys[pygame.K_ESCAPE]:
    done = True

Start moving the ball (before pygame.display.update()):

ball.move()
if ball.leaves_screen_bottom():
    # reset the ball position
    ball.rect.x = 200
    ball.rect.y = 300

Alright, if we did everything well, we'll see the following:

Each sprite is now rendered.

Collision Detection

Now that the paddle and the ball are moving, we need to start using some sort of collision detection so that:

  • The ball can jump back from the paddle
  • The ball can destroy and remove the brick it hits

The sprite module's pygame.sprite.groupcollide() method provides a very convenient way of detecting collisions. It takes four parameters:

  1. group1 - The sprite of groups we want to test collision for (with sprite group provided in the second parameter)
  2. group2 - The group of sprites
  3. dokill1 - Whether the sprite of the group1 should be removed in case of collision
  4. dokill2 - The same for group2
  5. collided - Collision detection strategy, a callback function

What we are very interested in is the last parameter. The sprite documentation lists the following possible callbacks for collision detection:

colliderect, colliderectratio, collidecircle, collidecircleratio, collide_mask

The thing is, we use a ball (that is basically a circle) and bricks (rectangles). If the ball was a rectangle too, it would be straightforward to use the collide_rect callback function.

So, how do we test the collision of a circle and a ball? There are two choices:

  1. Use math to calculate the intersection of a circle and rectangle
  2. Use the collide_mask method

The first option is not preferred at all; not only the math is not that simple to calculate intersection points between a circle and lines, there's a much better way to do this.

Namely, the collide_mask. It is not a mathematical approach though, it is rather checking whether two sprites' bitmap overlap. It could mean something like this very simplistically: did I draw a pixel for this sprite in this position? Yes. Did I draw a pixel for the same position for the other sprite? Yes, then it's a collision.

Alright, let's implement the collision! First, create a mask for each object:

self.mask = pygame.mask.from_surface(self.image)

Then implement the actual checks:

if pygame.sprite.collide_mask(ball, paddle):
    ball.flip_direction_y()

collided_bricks = pygame.sprite.groupcollide(
    brick_list, ball_list, True, False, pygame.sprite.collide_mask)
if collided_bricks:
    ball.flip_direction_y()

Translating these instructions to actual words it means that if the ball collides the paddle, then the ball's direction should be reversed. Also, if any brick of the brick list collides with any ball of the ball list (there's only one though) then the brick should be removed and the ball's vertical direction should be reversed.

Summary

That's it! We've just implemented a breakout game using pygame and managed to used sprites.

The source code can be downloaded here.

Addendum

If, for some reason, we don't want to use sprites and want to work only with basic shapes, detecting collisions might be a bit tricky. There's something though that simplifies the collision detection between a brick and ball a lot.

The bricks can be thought of as four segments. And for each segment, either the x or y coordinate is constant. That greatly reduces the complexity of the collision detecting. Let's look at how this can be done.

The equation of the circle is as follows:

Equation of the circle.

Which is basically a quadtraic equation and can be written in the following form:

Solving the equation for x.

We need to solve this equation for each segment. If there's a solution, then we have a collision.

class Util():
    def intersects(self, a, b, c, min, max):
        disc = b ** 2 - 4 * a * c
        if disc < 0:
            return False
        s1 = (-b + math.sqrt(disc)) / (2 * a)
        s2 = (-b - math.sqrt(disc)) / (2 * a)
        return (min <= s1 <= max) or (min <= s2 <= max)

class Ball():
    def collides_ball(self, ball):
        intersects_x = False
        intersects_y = False

        intersects_x = self.util.intersects(
            1, -2 * ball.x, (ball.y - self.y)**2 - ball.radius**2 + ball.x**2, self.x, self.x + self.WIDTH)
        intersects_x = intersects_x or self.util.intersects(
            1, -2 * ball.x, (ball.y - self.y - self.HEIGHT)**2 - ball.radius**2 + ball.x**2, self.x, self.x + self.WIDTH)

        intersects_y = self.util.intersects(
            1, -2 * ball.y, (ball.x - self.x)**2 - ball.radius**2 + ball.y**2, self.y, self.y + self.HEIGHT)
        intersects_y = intersects_y or self.util.intersects(
            1, -2 * ball.y, (ball.x - self.x - self.WIDTH)**2 - ball.radius**2 + ball.y**2, self.y, self.y + self.HEIGHT)

        return intersects_x, intersects_y

Of course, there might be better and more optimized solutions too, but this will be enough to get us started. The source code of the non-sprite version is available here.