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 surfacepygame.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(): likeconvert()but preserves the alpha channeldestination.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.
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.
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.
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
# 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.
# 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 surfacesurface.convert_alpha(): optimize with transparencyscreen.blit(surface, (x, y)): copy a surface onto anotherpygame.image.load(path): load an image from a file
Complete code
"""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
