What you will learn

This chapter moves your game from simple geometric shapes to textured surfaces. You will discover the central concept of Pygame: the Surface. You will learn to create images programmatically, manage transparency (alpha channel), and use blit() to draw one Surface onto another.

The concept

In Pygame, everything is a Surface. The screen is a Surface. An image loaded from a file is a Surface. Rendered text is a Surface. A Surface is simply an array of pixels in memory.

The key operations are:

  • pygame.Surface((w, h)): creates a new empty surface
  • pygame.Surface((w, h), pygame.SRCALPHA): creates a surface with alpha channel (per-pixel transparency)
  • surface.convert(): optimizes the surface format for display (faster to draw)
  • surface.convert_alpha(): like convert() but preserves the alpha channel
  • destination.blit(source, position): copies the source surface onto the destination at the given position

In production, you would load images from PNG files with pygame.image.load("sprite.png"). In this tutorial, we create our sprites programmatically to avoid depending on any external files. The principle is the same: you get a Surface that you draw with blit().

Step by step

1. Creating the paddle image

Our paddle goes from a simple rectangle to a vertical gradient. We create a Surface with SRCALPHA (per-pixel transparency), then draw line by line with varying brightness.

python
def create_paddle_image(width, height):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    for y in range(height):
        brightness = 200 + int(55 * (1 - y / height))
        pygame.draw.line(surf, (brightness, brightness, brightness),
                         (0, y), (width, y))
    # Rounded corners effect
    pygame.draw.rect(surf, (0, 0, 0, 0), (0, 0, 3, 3))
    pygame.draw.rect(surf, (0, 0, 0, 0), (width - 3, 0, 3, 3))
    return surf.convert_alpha()

Brightness ranges from 255 (white, at top) to 200 (light gray, at bottom), creating a subtle 3D effect. The corners are erased with transparent rectangles (0, 0, 0, 0) for a rounded look. The convert_alpha() call at the end optimizes the Surface for drawing with transparency.

2. Creating the ball image

The ball gains a luminous halo. We draw concentric circles that become progressively transparent, then the white core, and finally a small highlight.

python
def create_ball_image(radius):
    size = radius * 4
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    # Glow
    for r in range(radius * 2, radius, -1):
        alpha = int(100 * (1 - (r - radius) / radius))
        pygame.draw.circle(surf, (100, 100, 255, alpha),
                           (size // 2, size // 2), r)
    # Core
    pygame.draw.circle(surf, (255, 255, 255),
                       (size // 2, size // 2), radius)
    # Highlight
    pygame.draw.circle(surf, (255, 255, 255, 200),
                       (size // 2 - 2, size // 2 - 2), radius // 3)
    return surf.convert_alpha()

The Surface is larger than the ball itself (radius * 4) to leave room for the halo. The halo is light blue and semi-transparent. The highlight gives a sense of volume.

3. Creating brick images

Each brick gains a 3D effect with a highlight line on top and left, and a shadow on bottom and right.

python
def create_brick_image(width, height, color):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    # Main color
    pygame.draw.rect(surf, color, (0, 0, width, height))
    # Highlight top and left
    highlight = tuple(min(255, c + 50) for c in color)
    pygame.draw.line(surf, highlight, (1, 1), (width - 2, 1))
    pygame.draw.line(surf, highlight, (1, 1), (1, height - 2))
    # Shadow bottom and right
    shadow = tuple(max(0, c - 50) for c in color)
    pygame.draw.line(surf, shadow, (1, height - 1), (width - 1, height - 1))
    pygame.draw.line(surf, shadow, (width - 1, 1), (width - 1, height - 1))
    return surf.convert_alpha()

We pre-compute images for each row at game startup, rather than recreating them every frame. This is an important optimization: creating surfaces is expensive, copying them with blit() is fast.

4. Pre-creating images at startup

python
# Pre-create images
paddle_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)

# Pre-create brick images for each row
brick_images = []
for row in range(BRICK_ROWS):
    color = rainbow_color(row, BRICK_ROWS)
    brick_images.append(create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color))

5. Drawing with blit()

Instead of pygame.draw.rect() and pygame.draw.circle(), we now use screen.blit(image, position). The position is the top-left corner of the image.

python
# Draw bricks using images
for row in range(BRICK_ROWS):
    for col in range(BRICK_COLS):
        x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
        y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
        screen.blit(brick_images[row], (x, y))

# Draw paddle using image
screen.blit(paddle_img, (paddle_x, paddle_y))

# Draw ball using image (offset because the surface is larger)
ball_offset = BALL_RADIUS * 2
screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))

Note the ball offset: since its Surface is larger than the visible circle (because of the halo), we must subtract an offset so the visual center matches the logical position.

Key takeaways

Tip: to load an image from a file, use img = pygame.image.load("sprite.png").convert_alpha(). Never forget convert() or convert_alpha(): without this step, drawing will be much slower because Pygame will have to convert the format at each blit() call.

Common pitfall: the position passed to blit() is the top-left corner of the surface, not its center. To center a sprite, subtract half its width and height.

Common pitfall: if your image has a black background instead of being transparent, you probably forgot pygame.SRCALPHA when creating the Surface, or you are using convert() instead of convert_alpha().

Pygame functions used:

  • pygame.Surface((w, h), pygame.SRCALPHA): transparent surface
  • surface.convert_alpha(): optimize with transparency
  • screen.blit(surface, (x, y)): copy a surface onto another
  • pygame.image.load(path): load an image from a file

Complete code

python
"""Brick Breaker - Chapter 5: Displaying Images"""
import pygame
import sys

pygame.init()

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
BG_COLOR = (20, 20, 40)

PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_SPEED = 8
paddle_x = SCREEN_WIDTH // 2 - PADDLE_WIDTH // 2
paddle_y = SCREEN_HEIGHT - 40

BALL_RADIUS = 8
ball_x = SCREEN_WIDTH // 2
ball_y = SCREEN_HEIGHT // 2

BRICK_ROWS = 6
BRICK_COLS = 10
BRICK_WIDTH = 70
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_X = 30
BRICK_OFFSET_Y = 50


def rainbow_color(row, total_rows):
    hue = int(360 * row / total_rows)
    color = pygame.Color(0)
    color.hsva = (hue, 100, 100, 100)
    return (color.r, color.g, color.b)


# Create paddle image (gradient surface)
def create_paddle_image(width, height):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    for y in range(height):
        brightness = 200 + int(55 * (1 - y / height))
        pygame.draw.line(surf, (brightness, brightness, brightness),
                         (0, y), (width, y))
    # Rounded corners effect
    pygame.draw.rect(surf, (0, 0, 0, 0), (0, 0, 3, 3))
    pygame.draw.rect(surf, (0, 0, 0, 0), (width - 3, 0, 3, 3))
    return surf.convert_alpha()


# Create ball image (circle with glow)
def create_ball_image(radius):
    size = radius * 4
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    # Glow
    for r in range(radius * 2, radius, -1):
        alpha = int(100 * (1 - (r - radius) / radius))
        pygame.draw.circle(surf, (100, 100, 255, alpha),
                           (size // 2, size // 2), r)
    # Core
    pygame.draw.circle(surf, (255, 255, 255),
                       (size // 2, size // 2), radius)
    # Highlight
    pygame.draw.circle(surf, (255, 255, 255, 200),
                       (size // 2 - 2, size // 2 - 2), radius // 3)
    return surf.convert_alpha()


# Create brick image
def create_brick_image(width, height, color):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    # Main color
    pygame.draw.rect(surf, color, (0, 0, width, height))
    # Highlight top
    highlight = tuple(min(255, c + 50) for c in color)
    pygame.draw.line(surf, highlight, (1, 1), (width - 2, 1))
    pygame.draw.line(surf, highlight, (1, 1), (1, height - 2))
    # Shadow bottom
    shadow = tuple(max(0, c - 50) for c in color)
    pygame.draw.line(surf, shadow, (1, height - 1), (width - 1, height - 1))
    pygame.draw.line(surf, shadow, (width - 1, 1), (width - 1, height - 1))
    return surf.convert_alpha()


screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 5")
clock = pygame.time.Clock()
font = pygame.font.SysFont("monospace", 14)

# Pre-create images
paddle_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)

# Pre-create brick images for each row
brick_images = []
for row in range(BRICK_ROWS):
    color = rainbow_color(row, BRICK_ROWS)
    brick_images.append(create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color))

use_mouse = False

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            if event.key == pygame.K_m:
                use_mouse = not use_mouse

    if use_mouse:
        mx, _ = pygame.mouse.get_pos()
        paddle_x = mx - PADDLE_WIDTH // 2
    else:
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            paddle_x -= PADDLE_SPEED
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            paddle_x += PADDLE_SPEED

    paddle_x = max(0, min(paddle_x, SCREEN_WIDTH - PADDLE_WIDTH))

    screen.fill(BG_COLOR)

    # Draw bricks using images
    for row in range(BRICK_ROWS):
        for col in range(BRICK_COLS):
            x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
            y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
            screen.blit(brick_images[row], (x, y))

    # Draw paddle using image
    screen.blit(paddle_img, (paddle_x, paddle_y))

    # Draw ball using image
    ball_offset = BALL_RADIUS * 2
    screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))

    info = font.render("[M] Mouse/Keyboard  [Arrows] Move  [ESC] Quit",
                       True, (150, 150, 150))
    screen.blit(info, (10, SCREEN_HEIGHT - 25))

    pygame.display.flip()
    clock.tick(FPS)

pygame.quit()
sys.exit()

Download

Download the source code for this chapter: brick-breaker-ch05.zip